diff --git a/src/Umbraco.Core/Actions/ActionAssignDomain.cs b/src/Umbraco.Core/Actions/ActionAssignDomain.cs index 452ca51549d4..b8fd8a46500c 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 5be16a01c912..2620888a30e6 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 char Letter => ActionLetter; - - /// - public bool ShowInNotifier => false; - - /// - public bool CanBePermissionAssigned => true; - - /// - public string Icon => string.Empty; - - /// - public string Alias => "browse"; - - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - } + public const char ActionLetter = 'F'; + + /// + public char Letter => ActionLetter; + + /// + public bool ShowInNotifier => false; + + /// + public bool CanBePermissionAssigned => true; + + /// + public string Icon => string.Empty; + + /// + public string Alias => "browse"; + + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; } diff --git a/src/Umbraco.Core/Actions/ActionCollection.cs b/src/Umbraco.Core/Actions/ActionCollection.cs index 1e396952a20a..b204075b8869 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 58e70e4a2aa2..aac1556234e8 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 83a855d1ff0a..02bb17166f17 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 806868af4023..85490b42f818 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 85c9b39dff29..7d9c4e6a03f8 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 0f8b4b830550..a40d03d096f9 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 25e85cd377c1..31056632ed5c 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 3f1e855cff5f..9d1975b852b8 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 10684a69e2ed..0a5ac8ace8a3 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 02f77d686235..e07b0935bc97 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 164c93e2d5da..79e00f446478 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 fff7cc86525d..08afe7e2db4f 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 565a8469c52d..5aada555d3d6 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 1f87bfcc3c5e..b77a44c7307c 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 654b71661d6f..bf15ee4e3ba4 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 6e9ec8506b97..c8a83f002e0d 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 char Letter => ActionLetter; + public const char ActionLetter = 'Z'; - /// - public string Alias => "unpublish"; + /// + public char Letter => ActionLetter; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; + /// + public string Alias => "unpublish"; - /// - public string Icon => "circle-dotted"; + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - /// - public bool ShowInNotifier => false; + /// + public string Icon => "circle-dotted"; - /// - 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 3f8092c1fc04..4af2410cc445 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 2d9876afc647..f57e697a2e62 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 71eabd2f0dc3..7a438dece660 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 - - /// - /// 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) - { - return Attempt.Succeed(status, result); - } + /// 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 failed attempt. - /// - /// The type of the attempted operation result. - /// The failed attempt. - public static Attempt Fail() - { - return Attempt.Fail(); - } + /// + /// 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 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. + /// + /// The type of the attempted operation result. + /// The failed attempt. + public static Attempt Fail() => Attempt.Fail(); - /// - /// 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. + /// + /// 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 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 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 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, 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 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) - { - return Attempt.If(condition, result); - } + /// + /// 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 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 5cf85964ccc1..2969755d9450 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 { + // 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) + { + Success = success; + Result = result; + Exception = exception; + } + /// - /// Represents the result of an operation attempt. + /// Gets a value indicating whether this was successful. /// - /// The type of the attempted operation result. - [Serializable] - public struct Attempt - { - // private - use Succeed() or Fail() methods to create attempts - private Attempt(bool success, TResult? result, Exception? exception) - { - Success = success; - Result = result; - Exception = exception; - } + public bool Success { get; } - /// - /// 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; - } + /// + /// Gets the exception associated with an unsuccessful attempt. + /// + public Exception? Exception { get; } - return value; - } + /// + /// Gets the attempt result. + /// + public TResult? Result { get; } - // optimize, use a singleton failed attempt - private static readonly Attempt Failed = new Attempt(false, default(TResult), null); + /// + /// 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 successful attempt. - public static Attempt Succeed() + /// + /// Gets the attempt result, if successful, else a default value. + /// + public TResult ResultOr(TResult value) + { + if (Success && Result is not null) { - return new Attempt(true, default(TResult), null); + return Result; } - /// - /// 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); - } + return value; + } - /// - /// Creates a failed attempt. - /// - /// The failed attempt. - public static Attempt Fail() - { - return Failed; - } + /// + /// Creates a successful attempt. + /// + /// The successful attempt. + public static Attempt Succeed() => new(true, default, null); - /// - /// 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 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 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. + /// + /// The failed attempt. + public static Attempt Fail() => Failed; - /// - /// 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 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 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 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 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); - } + /// + /// 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); - /// - /// 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; - } - } + /// + /// 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 65a3e483344b..e88465b3ad3a 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; } + Success = success; + Result = result; + Status = status; + Exception = exception; + } - /// - /// Gets the exception associated with an unsuccessful attempt. - /// - public Exception? Exception { get; } + /// + /// Gets a value indicating whether this was successful. + /// + public bool Success { get; } - /// - /// Gets the attempt result. - /// - public TResult Result { get; } + /// + /// Gets the exception associated with an unsuccessful attempt. + /// + public Exception? Exception { get; } - /// - /// Gets the attempt status. - /// - public TStatus Status { get; } + /// + /// Gets the attempt result. + /// + public TResult Result { 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; - } + /// + /// Gets the attempt status. + /// + public TStatus Status { get; } - /// - /// 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); - } + /// + /// 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 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 successful attempt. + /// + /// The status of the attempt. + /// The successful attempt. + public static Attempt Succeed(TStatus status) => + new Attempt(true, default, 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 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 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. + /// + /// 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 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 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 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 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 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 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, 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); - } + /// + /// 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); - /// - /// 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; - } - } + /// + /// 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 f5e92cc1166e..0f1f242ed01f 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(T) : result.TryConvertTo().Result; - } + 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 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) - { - var result = provider.SearchByKey(keyStartsWith); - return result.Select(x => x.TryConvertTo().Result); - } + 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) - { - var result = provider.SearchByRegex(regexString); - 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) + public static T? GetCacheItem(this IAppCache provider, string cacheKey) + { + var result = provider.Get(cacheKey); + if (result == null) { - var result = provider.Get(cacheKey); - if (result == null) - { - return default(T); - } - return result.TryConvertTo().Result; + return default; } - public static T? GetCacheItem(this IAppCache provider, string cacheKey, Func getCacheItem) + return result.TryConvertTo().Result; + } + + public static T? GetCacheItem(this IAppCache provider, string cacheKey, Func getCacheItem) + { + var result = provider.Get(cacheKey, () => getCacheItem()); + if (result == null) { - var result = provider.Get(cacheKey, () => getCacheItem()); - if (result == null) - { - return default(T); - } - return result.TryConvertTo().Result; + return default; } + + return result.TryConvertTo().Result; } } diff --git a/src/Umbraco.Core/Cache/AppCaches.cs b/src/Umbraco.Core/Cache/AppCaches.cs index a04ece0d04af..faca2e14f4f0 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) - { - 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(NoAppCache.Instance, NoAppCache.Instance, new IsolatedCaches(_ => NoAppCache.Instance)); - /// - /// 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(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 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 runtime cache. + /// + /// + /// The runtime cache is the main application cache. + /// + public IAppPolicyCache RuntimeCache { get; } - /// - /// 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 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; } - /// - /// Gets the runtime cache. - /// - /// - /// The runtime cache is the main application cache. - /// - public IAppPolicyCache RuntimeCache { get; } + public static AppCaches Create(IRequestCache requestCache) => + new( + new DeepCloneAppCache(new ObjectCacheAppCache()), + requestCache, + new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()))); - /// - /// 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 void Dispose() => - public static AppCaches Create(IRequestCache requestCache) - { - return new AppCaches( - new DeepCloneAppCache(new ObjectCacheAppCache()), - requestCache, - new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()))); - } + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - 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 53e45bbb2e0d..1cf3b1461e0f 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 ConcurrentDictionary _caches = new ConcurrentDictionary(); + private readonly Func _cacheFactory; - /// - /// Initializes a new instance of the class. - /// - /// - protected AppPolicedCacheDictionary(Func cacheFactory) - { - _cacheFactory = cacheFactory; - } + private readonly ConcurrentDictionary _caches = new(); + private bool _disposedValue; - /// - /// Gets the internal cache factory, for tests only! - /// - private readonly Func _cacheFactory; - private bool _disposedValue; + /// + /// Initializes a new instance of the class. + /// + /// + protected AppPolicedCacheDictionary(Func cacheFactory) => _cacheFactory = cacheFactory; - /// - /// Gets or creates a cache. - /// - public IAppPolicyCache GetOrCreate(TKey key) - => _caches.GetOrAdd(key, k => _cacheFactory(k)); + public void Dispose() => - /// - /// Tries to get a cache. - /// - protected Attempt Get(TKey key) - => _caches.TryGetValue(key, out var cache) ? Attempt.Succeed(cache) : Attempt.Fail(); + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); - /// - /// Removes a cache. - /// - public void Remove(TKey key) - { - _caches.TryRemove(key, out _); - } + /// + /// Gets or creates a cache. + /// + public IAppPolicyCache GetOrCreate(TKey key) + => _caches.GetOrAdd(key, k => _cacheFactory(k)); - /// - /// Removes all caches. - /// - public void RemoveAll() - { - _caches.Clear(); - } + /// + /// Removes a cache. + /// + public void Remove(TKey key) => _caches.TryRemove(key, out _); + + /// + /// Removes all caches. + /// + public void RemoveAll() => _caches.Clear(); - /// - /// Clears a cache. - /// - protected void ClearCache(TKey key) + /// + /// Clears all caches. + /// + public void ClearAllCaches() + { + foreach (IAppPolicyCache cache in _caches.Values) { - if (_caches.TryGetValue(key, out var cache)) - cache.Clear(); + cache.Clear(); } + } + + /// + /// Tries to get a cache. + /// + protected Attempt Get(TKey key) + => _caches.TryGetValue(key, out IAppPolicyCache? cache) + ? Attempt.Succeed(cache) + : Attempt.Fail(); - /// - /// Clears all caches. - /// - public void ClearAllCaches() + /// + /// Clears a cache. + /// + protected void ClearCache(TKey key) + { + if (_caches.TryGetValue(key, out IAppPolicyCache? cache)) { - foreach (var cache in _caches.Values) - cache.Clear(); + cache.Clear(); } + } - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - 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 582915fb2e21..11ddd8a18349 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 -{ - public sealed class ApplicationCacheRefresher : CacheRefresherBase - { - public ApplicationCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("B15F34A1-BC1D-4F8B-8369-3222728AB4C8"); +namespace Umbraco.Cms.Core.Cache; - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Application Cache Refresher"; +public sealed class ApplicationCacheRefresher : CacheRefresherBase +{ + public static readonly Guid UniqueId = Guid.Parse("B15F34A1-BC1D-4F8B-8369-3222728AB4C8"); - #endregion + public ApplicationCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) + { + } - #region 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); - } + public override void RefreshAll() + { + AppCaches.RuntimeCache.Clear(CacheKeys.ApplicationsCacheKey); + base.RefreshAll(); + } - public override void Remove(int id) - { - AppCaches.RuntimeCache.Clear(CacheKeys.ApplicationsCacheKey); - base.Remove(id); - } + public override void Refresh(int id) + { + Remove(id); + base.Refresh(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 acabe0fcc4d5..04ae44a64743 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 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 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 7b962065c561..849d42309a41 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 b9dc7f598444..301f6bbdaf1b 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 +namespace Umbraco.Cms.Core.Cache; + +public class CacheRefresherCollection : BuilderCollectionBase { - public class CacheRefresherCollection : BuilderCollectionBase + public CacheRefresherCollection(Func> items) + : base(items) { - public CacheRefresherCollection(Func> items) : base(items) - { - } - - public ICacheRefresher? this[Guid id] - => this.FirstOrDefault(x => x.RefresherUniqueId == id); } + + 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 34a274a17799..79b44ab53d14 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 bd41ee9d9b7b..40bab16b12dd 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 { - /// - /// A that uses ActivatorUtilities to create the instances - /// - public sealed class CacheRefresherNotificationFactory : ICacheRefresherNotificationFactory - { - private readonly IServiceProvider _serviceProvider; + private readonly IServiceProvider _serviceProvider; - public CacheRefresherNotificationFactory(IServiceProvider serviceProvider) => _serviceProvider = 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 }); - } + /// + /// Create a using ActivatorUtilities + /// + /// 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 ff55a201f588..a515d5c5d197 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.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; - } + _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 + #region Define - public static readonly Guid UniqueId = Guid.Parse("900A4FBE-DF3C-41E6-BB77-BE896CD158EA"); + public static readonly Guid UniqueId = Guid.Parse("900A4FBE-DF3C-41E6-BB77-BE896CD158EA"); - public override Guid RefresherUniqueId => UniqueId; + public override Guid RefresherUniqueId => UniqueId; - public override string Name => "ContentCacheRefresher"; + public override string Name => "ContentCacheRefresher"; - #endregion + #endregion - #region Refresher + #region Refresher - public override void Refresh(JsonPayload[] payloads) + 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)) { - AppCaches.RuntimeCache.ClearOfType(); - AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey); + // By INT Id + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - var idsRemoved = new HashSet(); - var isolatedCache = AppCaches.IsolatedCaches.GetOrCreate(); + // By GUID Key + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); - foreach (var payload in payloads.Where(x => x.Id != default)) + _idKeyMap.ClearCache(payload.Id); + + // remove those that are in the branch + if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) { - //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); - } + var pathid = "," + payload.Id + ","; + isolatedCache.ClearOfType((k, v) => v.Path?.Contains(pathid) ?? false); } - if (idsRemoved.Count > 0) + // 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)) { - 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()); - } + idsRemoved.Add(payload.Id); } - - // 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(); + if (idsRemoved.Count > 0) + { + var assignedDomains = _domainService.GetAll(true) + ?.Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value)).ToList(); - public override void Refresh(int id) => throw new NotSupportedException(); + 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()); + } + } - public override void Refresh(Guid id) => throw new NotSupportedException(); + // 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... - public override void Remove(int id) => throw new NotSupportedException(); + // TODO: what about this? + // should rename it, and then, this is only for Deploy, and then, ??? + // if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + // ... + NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, payloads); - #endregion + base.Refresh(payloads); + } - #region Json + // these events should never trigger + // everything should be PAYLOAD/JSON + public override void RefreshAll() => throw new NotSupportedException(); - /// - /// 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); + public override void Refresh(int id) => throw new NotSupportedException(); - if (payloads.Any(x => x.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) || publishedChanged) - { - // when a public version changes - appCaches.ClearPartialViewCache(); - } - } + public override void Refresh(Guid id) => throw new NotSupportedException(); - public class JsonPayload - { - public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) - { - Id = id; - Key = key; - ChangeTypes = changeTypes; - } + public override void Remove(int id) => throw new NotSupportedException(); - public int Id { get; } - public Guid? Key { get; } - public TreeChangeTypes ChangeTypes { get; } - } + #endregion - #endregion + #region Json - #region Indirect + /// + /// 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); - public static void RefreshContentTypes(AppCaches appCaches) + if (payloads.Any(x => x.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) || publishedChanged) { - // 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 - + // when a public version changes appCaches.ClearPartialViewCache(); + } + } - appCaches.IsolatedCaches.ClearCache(); - appCaches.IsolatedCaches.ClearCache(); + public class JsonPayload + { + public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) + { + Id = id; + Key = key; + ChangeTypes = changeTypes; } - #endregion + 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 9a709e9a9f6e..e1a82d6108fd 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.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) + { + _publishedSnapshotService = publishedSnapshotService; + _publishedModelFactory = publishedModelFactory; + _idKeyMap = idKeyMap; + _contentTypeCommonRepository = contentTypeCommonRepository; + } + + #region Json + + public class JsonPayload { - 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) + public JsonPayload(string itemType, int id, ContentTypeChangeTypes changeTypes) { - _publishedSnapshotService = publishedSnapshotService; - _publishedModelFactory = publishedModelFactory; - _idKeyMap = idKeyMap; - _contentTypeCommonRepository = contentTypeCommonRepository; + ItemType = itemType; + Id = id; + ChangeTypes = changeTypes; } - #region Define + public string ItemType { get; } - public static readonly Guid UniqueId = Guid.Parse("6902E22C-9C10-483C-91F3-66B7CAE9E2F5"); + public int Id { get; } - public override Guid RefresherUniqueId => UniqueId; + public ContentTypeChangeTypes ChangeTypes { get; } + } - public override string Name => "Content Type Cache Refresher"; + #endregion - #endregion + #region Define - #region Refresher + public static readonly Guid UniqueId = Guid.Parse("6902E22C-9C10-483C-91F3-66B7CAE9E2F5"); - public override void Refresh(JsonPayload[] payloads) + 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)) { - // 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); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); } + if (payloads.Any(x => x.ItemType == typeof(IMediaType).Name)) + { + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + } + + if (payloads.Any(x => x.ItemType == typeof(IMemberType).Name)) + { + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + } - public override void RefreshAll() + foreach (var id in payloads.Select(x => x.Id)) { - throw new NotSupportedException(); + _idKeyMap.ClearCache(id); } - public override void Refresh(int id) + if (payloads.Any(x => x.ItemType == typeof(IContentType).Name)) { - throw new NotSupportedException(); + // don't try to be clever - refresh all + ContentCacheRefresher.RefreshContentTypes(AppCaches); } - public override void Refresh(Guid id) + if (payloads.Any(x => x.ItemType == typeof(IMediaType).Name)) { - throw new NotSupportedException(); + // don't try to be clever - refresh all + MediaCacheRefresher.RefreshMediaTypes(AppCaches); } - public override void Remove(int id) + if (payloads.Any(x => x.ItemType == typeof(IMemberType).Name)) { - throw new NotSupportedException(); + // don't try to be clever - refresh all + MemberCacheRefresher.RefreshMemberTypes(AppCaches); } - #endregion + // refresh the models and cache + _publishedModelFactory.WithSafeLiveFactoryReset(() => + _publishedSnapshotService.Notify(payloads)); - #region Json + // now we can trigger the event + base.Refresh(payloads); + } - public class JsonPayload - { - public JsonPayload(string itemType, int id, ContentTypeChangeTypes changeTypes) - { - ItemType = itemType; - Id = id; - ChangeTypes = changeTypes; - } + public override void RefreshAll() => throw new NotSupportedException(); - public string ItemType { get; } + public override void Refresh(int id) => throw new NotSupportedException(); - public int Id { get; } + public override void Refresh(Guid id) => throw new NotSupportedException(); - public ContentTypeChangeTypes ChangeTypes { get; } - } + public override void Remove(int id) => throw new NotSupportedException(); - #endregion - } + #endregion } diff --git a/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs index 44d730be8389..ea661c549854 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.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 + _publishedSnapshotService = publishedSnapshotService; + _publishedModelFactory = publishedModelFactory; + _idKeyMap = idKeyMap; + } - #region Refresher + #region Json - public override void Refresh(JsonPayload[] payloads) + public class JsonPayload + { + public JsonPayload(int id, Guid key, bool removed) { - //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 + Id = id; + Key = key; + Removed = removed; + } - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); + public int Id { get; } - var dataTypeCache = AppCaches.IsolatedCaches.Get(); + public Guid Key { get; } - foreach (var payload in payloads) - { - _idKeyMap.ClearCache(payload.Id); + public bool Removed { get; } + } - if (dataTypeCache.Success) - { - dataTypeCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - } - } + #endregion - // TODO: not sure I like these? - TagsValueConverter.ClearCaches(); - SliderValueConverter.ClearCaches(); + #region Define - // refresh the models and cache + public static readonly Guid UniqueId = Guid.Parse("35B16C25-A17E-45D7-BC8F-EDAB1DCC28D2"); - _publishedModelFactory.WithSafeLiveFactoryReset(() => - _publishedSnapshotService.Notify(payloads)); + public override Guid RefresherUniqueId => UniqueId; - base.Refresh(payloads); - } + public override string Name => "Data Type Cache Refresher"; - // these events should never trigger - // everything should be PAYLOAD/JSON + #endregion - public override void RefreshAll() - { - throw new NotSupportedException(); - } + #region Refresher - public override void Refresh(int id) + 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) { - throw new NotSupportedException(); - } + _idKeyMap.ClearCache(payload.Id); - public override void Refresh(Guid id) - { - throw new NotSupportedException(); + if (dataTypeCache.Success) + { + dataTypeCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + } } - public override void Remove(int id) - { - throw new NotSupportedException(); - } + // TODO: not sure I like these? + TagsValueConverter.ClearCaches(); + SliderValueConverter.ClearCaches(); - #endregion + // refresh the models and cache + _publishedModelFactory.WithSafeLiveFactoryReset(() => + _publishedSnapshotService.Notify(payloads)); - #region Json + base.Refresh(payloads); + } - public class JsonPayload - { - public JsonPayload(int id, Guid key, bool removed) - { - Id = id; - Key = key; - Removed = removed; - } + // these events should never trigger + // everything should be PAYLOAD/JSON + public override void RefreshAll() => throw new NotSupportedException(); - public int Id { get; } + public override void Refresh(int id) => throw new NotSupportedException(); - public Guid Key { get; } + public override void Refresh(Guid id) => throw new NotSupportedException(); - public bool Removed { get; } - } + public override void Remove(int id) => throw new NotSupportedException(); - #endregion - } + #endregion } diff --git a/src/Umbraco.Core/Cache/DeepCloneAppCache.cs b/src/Umbraco.Core/Cache/DeepCloneAppCache.cs index 60a0d8d7b36d..da86be4b703a 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); + throw new InvalidOperationException($"A {type} cannot wrap another instance of itself."); + } - if (innerCache.GetType() == type) - throw new InvalidOperationException($"A {type} cannot wrap another instance of itself."); + InnerCache = innerCache; + } - InnerCache = innerCache; - } + /// + /// Gets the inner cache. + /// + private IAppPolicyCache InnerCache { get; } - /// - /// 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) + /// + 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) - { - 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); - } + // 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) - { - return InnerCache.SearchByKey(keyStartsWith) - .Select(CheckCloneableAndTracksChanges); - } + /// + public IEnumerable SearchByKey(string keyStartsWith) => + InnerCache.SearchByKey(keyStartsWith) + .Select(CheckCloneableAndTracksChanges); - /// - public IEnumerable SearchByRegex(string regex) - { - return InnerCache.SearchByRegex(regex) - .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) + /// + 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); + Lazy result = SafeLazy.GetSafeLazy(factory); + var value = result + .Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache - // 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) - { - InnerCache.Insert(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); - }, timeout, isSliding, dependentFiles); - } + // clone / reset to go into the cache + return CheckCloneableAndTracksChanges(cached); + } - /// - public void Clear() + /// + public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) => + InnerCache.Insert( + key, + () => { - InnerCache.Clear(); - } + 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 void Clear(string key) - { - InnerCache.Clear(key); - } + // 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 ClearOfType(Type type) - { - InnerCache.ClearOfType(type); - } + /// + public void Clear() => InnerCache.Clear(); - /// - public void ClearOfType() - { - InnerCache.ClearOfType(); - } + /// + public void Clear(string key) => InnerCache.Clear(key); - /// - public void ClearOfType(Func predicate) - { - InnerCache.ClearOfType(predicate); - } + /// + public void ClearOfType(Type type) => InnerCache.ClearOfType(type); - /// - public void ClearByKey(string keyStartsWith) - { - InnerCache.ClearByKey(keyStartsWith); - } + /// + public void ClearOfType() => InnerCache.ClearOfType(); - /// - public void ClearByRegex(string regex) - { - InnerCache.ClearByRegex(regex); - } + /// + public void ClearOfType(Func predicate) => InnerCache.ClearOfType(predicate); - private static object? CheckCloneableAndTracksChanges(object? input) - { - if (input is IDeepCloneable cloneable) - { - input = cloneable.DeepClone(); - } + /// + public void ClearByKey(string keyStartsWith) => InnerCache.ClearByKey(keyStartsWith); + + /// + public void ClearByRegex(string regex) => InnerCache.ClearByRegex(regex); + + public void Dispose() => - // reset dirty initial properties - if (input is IRememberBeingDirty tracksChanges) + // 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) { - tracksChanges.ResetDirtyProperties(false); - input = tracksChanges; + InnerCache.DisposeIfDisposable(); } - return input; + _disposedValue = true; } + } - protected virtual void Dispose(bool disposing) + private static object? CheckCloneableAndTracksChanges(object? input) + { + if (input is IDeepCloneable cloneable) { - if (!_disposedValue) - { - if (disposing) - { - InnerCache.DisposeIfDisposable(); - } - - _disposedValue = true; - } + input = cloneable.DeepClone(); } - public void Dispose() + // reset dirty initial properties + if (input is IRememberBeingDirty tracksChanges) { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + tracksChanges.ResetDirtyProperties(false); + input = tracksChanges; } + + return input; } } diff --git a/src/Umbraco.Core/Cache/DictionaryAppCache.cs b/src/Umbraco.Core/Cache/DictionaryAppCache.cs index 296050a36109..5bf584830967 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 - { - /// - /// Gets the internal items dictionary, for tests only! - /// - private readonly ConcurrentDictionary _items = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _items = new(); - public int Count => _items.Count; + public int Count => _items.Count; - /// - public bool IsAvailable => true; + /// + public bool IsAvailable => true; - /// - public virtual object? Get(string key) - { - return _items.TryGetValue(key, out var value) ? value : null; - } + /// + public virtual object? Get(string key) => _items.TryGetValue(key, out var value) ? value : null; - /// - public virtual object? Get(string key, Func factory) - { - return _items.GetOrAdd(key, _ => factory()); - } + /// + 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 Set(string key, object? value) => _items.TryAdd(key, value); - public bool Remove(string key) => _items.TryRemove(key, out _); + public bool Remove(string key) => _items.TryRemove(key, out _); - /// - public virtual IEnumerable SearchByKey(string keyStartsWith) + /// + public virtual IEnumerable SearchByKey(string keyStartsWith) + { + var items = new List(); + foreach ((string key, object? value) in _items) { - var items = new List(); - foreach (var (key, value) in _items) - if (key.InvariantStartsWith(keyStartsWith)) - items.Add(value); - return items; + if (key.InvariantStartsWith(keyStartsWith)) + { + items.Add(value); + } } - /// - 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; - } + return items; + } - /// - public virtual void Clear() + /// + public IEnumerable SearchByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + var items = new List(); + foreach ((string key, object? value) in _items) { - _items.Clear(); + if (compiled.IsMatch(key)) + { + items.Add(value); + } } - /// - public virtual void Clear(string key) - { - _items.TryRemove(key, out _); - } + return items; + } - /// - public virtual void ClearOfType(Type type) - { - _items.RemoveAll(kvp => kvp.Value != null && kvp.Value.GetType() == type); - } + /// + public virtual void Clear() => _items.Clear(); - /// - public virtual void ClearOfType() - { - var typeOfT = typeof(T); - ClearOfType(typeOfT); - } + /// + public virtual void Clear(string key) => _items.TryRemove(key, out _); - /// - 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 ClearOfType(Type type) => + _items.RemoveAll(kvp => kvp.Value != null && kvp.Value.GetType() == type); - /// - public virtual void ClearByKey(string keyStartsWith) - { - _items.RemoveAll(kvp => kvp.Key.InvariantStartsWith(keyStartsWith)); - } + /// + public virtual void ClearOfType() + { + Type typeOfT = typeof(T); + ClearOfType(typeOfT); + } - /// - public virtual void ClearByRegex(string regex) - { - var compiled = new Regex(regex, RegexOptions.Compiled); - _items.RemoveAll(kvp => compiled.IsMatch(kvp.Key)); - } + /// + 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 IEnumerator> GetEnumerator() => _items.GetEnumerator(); + /// + public virtual void ClearByKey(string keyStartsWith) => + _items.RemoveAll(kvp => kvp.Key.InvariantStartsWith(keyStartsWith)); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + /// + 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 dbe84b114e4e..c10640986caa 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 -{ - public sealed class DictionaryCacheRefresher : CacheRefresherBase - { - public DictionaryCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator , factory) - { } - - #region Define +namespace Umbraco.Cms.Core.Cache; - public static readonly Guid UniqueId = Guid.Parse("D1D7E227-F817-4816-BFE9-6C39B6152884"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Dictionary Cache Refresher"; +public sealed class DictionaryCacheRefresher : CacheRefresherBase +{ + public static readonly Guid UniqueId = Guid.Parse("D1D7E227-F817-4816-BFE9-6C39B6152884"); - #endregion + public DictionaryCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) + { + } - #region Refresher + public override Guid RefresherUniqueId => UniqueId; - public override void Refresh(int id) - { - ClearAllIsolatedCacheByEntityType(); - base.Refresh(id); - } + public override string Name => "Dictionary Cache Refresher"; - public override void Remove(int id) - { - ClearAllIsolatedCacheByEntityType(); - base.Remove(id); - } + public override void Refresh(int id) + { + ClearAllIsolatedCacheByEntityType(); + base.Refresh(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 95c17b946db3..0adb0ea37083 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 { + private readonly CacheRefresherCollection _cacheRefreshers; + private readonly IServerMessenger _serverMessenger; + + public DistributedCache(IServerMessenger serverMessenger, CacheRefresherCollection cacheRefreshers) + { + _serverMessenger = serverMessenger; + _cacheRefreshers = cacheRefreshers; + } + + #region Core notification methods + /// - /// Represents the entry point into Umbraco's distributed cache infrastructure. + /// 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. /// - /// - /// 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 - /// + /// This method is much better for performance because it does not need to re-lookup object instances. /// - public sealed class DistributedCache + public void Refresh(Guid refresherGuid, Func getNumericId, params T[] instances) { - private readonly IServerMessenger _serverMessenger; - private readonly CacheRefresherCollection _cacheRefreshers; - - public DistributedCache(IServerMessenger serverMessenger, CacheRefresherCollection cacheRefreshers) + if (refresherGuid == Guid.Empty || instances.Length == 0 || getNumericId == null) { - _serverMessenger = serverMessenger; - _cacheRefreshers = cacheRefreshers; + return; } - #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); - } + _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) + // helper method to get an ICacheRefresher by its unique identifier + private ICacheRefresher GetRefresherById(Guid refresherGuid) + { + ICacheRefresher? refresher = _cacheRefreshers[refresherGuid]; + if (refresher == null) { - if (refresherGuid == Guid.Empty || id == default(int)) return; - - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - id); + throw new InvalidOperationException($"No cache refresher found with id {refresherGuid}"); } - /// - /// 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; + return refresher; + } - _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, int id) + { + if (refresherGuid == Guid.Empty || id == default) + { + return; } - // 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), + id); + } - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - payload); + /// + /// 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; } - // 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), + id); + } - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - payloads.ToArray()); + // 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; } - ///// - ///// 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.QueueRefresh( + GetRefresherById(refresherGuid), + payload); + } - _serverMessenger.QueueRefreshAll( - GetRefresherById(refresherGuid)); + // so deal with IEnumerable + public void RefreshByPayload(Guid refresherGuid, IEnumerable payloads) + where TPayload : class + { + if (refresherGuid == Guid.Empty || payloads == null) + { + return; } - /// - /// 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.QueueRefresh( + GetRefresherById(refresherGuid), + payloads.ToArray()); + } - _serverMessenger.QueueRemove( - GetRefresherById(refresherGuid), - id); - } + ///// + ///// 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 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) + /// + /// 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) { - _serverMessenger.QueueRemove( - GetRefresherById(refresherGuid), - getNumericId, - instances); + return; } - #endregion + _serverMessenger.QueueRefreshAll( + GetRefresherById(refresherGuid)); + } - // helper method to get an ICacheRefresher by its unique identifier - private ICacheRefresher GetRefresherById(Guid 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) { - ICacheRefresher? refresher = _cacheRefreshers[refresherGuid]; - if (refresher == null) - { - throw new InvalidOperationException($"No cache refresher found with id {refresherGuid}"); - } - - return refresher; + 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 28e62c854d6b..a6e46ee2e48b 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.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 int Id { get; } - public static readonly Guid UniqueId = Guid.Parse("11290A79-4B57-4C99-AD72-7748A3CF38AF"); - - public override Guid RefresherUniqueId => UniqueId; + public DomainChangeTypes ChangeType { get; } + } - public override string Name => "Domain Cache Refresher"; + #endregion - #endregion + #region Define - #region Refresher + public static readonly Guid UniqueId = Guid.Parse("11290A79-4B57-4C99-AD72-7748A3CF38AF"); - public override void Refresh(JsonPayload[] payloads) - { - ClearAllIsolatedCacheByEntityType(); + public override Guid RefresherUniqueId => UniqueId; - // 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... + public override string Name => "Domain Cache Refresher"; - // notify - _publishedSnapshotService.Notify(payloads); - // then trigger event - base.Refresh(payloads); - } + #endregion - // these events should never trigger - // everything should be PAYLOAD/JSON + #region Refresher - public override void RefreshAll() => throw new NotSupportedException(); + public override void Refresh(JsonPayload[] payloads) + { + ClearAllIsolatedCacheByEntityType(); - public override void Refresh(int id) => throw new NotSupportedException(); + // 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... - public override void Refresh(Guid id) => throw new NotSupportedException(); + // notify + _publishedSnapshotService.Notify(payloads); - public override void Remove(int id) => throw new NotSupportedException(); + // then trigger event + base.Refresh(payloads); + } - #endregion + // these events should never trigger + // everything should be PAYLOAD/JSON + public override void RefreshAll() => throw new NotSupportedException(); - #region Json + public override void Refresh(int id) => throw new NotSupportedException(); - public class JsonPayload - { - public JsonPayload(int id, DomainChangeTypes changeType) - { - Id = id; - ChangeType = changeType; - } + public override void Refresh(Guid id) => throw new NotSupportedException(); - public int Id { get; } + public override void Remove(int id) => throw new NotSupportedException(); - public DomainChangeTypes ChangeType { get; } - } - - #endregion - - } + #endregion } diff --git a/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs b/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs index 6c3b8855d2a2..6476c76f96f3 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(); - /// - /// Gets the internal items dictionary, for tests only! - /// - private readonly ConcurrentDictionary> _items = new ConcurrentDictionary>(); + public IEnumerable Keys => _items.Keys; - public IEnumerable Keys => _items.Keys; + public int Count => _items.Count; - 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 + } - /// - public object? Get(string cacheKey) + /// + public object? Get(string cacheKey, Func getCacheItem) + { + Lazy? result = _items.GetOrAdd(cacheKey, k => SafeLazy.GetSafeLazy(getCacheItem)); + + 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) - { - var result = _items.GetOrAdd(cacheKey, k => SafeLazy.GetSafeLazy(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 + } - var value = result.Value; // will not throw (safe lazy) - if (!(value is SafeLazy.ExceptionHolder eh)) - return value; + /// + public IEnumerable SearchByKey(string keyStartsWith) => + _items + .Where(kvp => kvp.Key.InvariantStartsWith(keyStartsWith)) + .Select(kvp => SafeLazy.GetSafeLazyValue(kvp.Value)) + .Where(x => x != null); - // 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 + /// + 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); + } - _items.TryRemove(cacheKey, out result); - eh.Exception.Throw(); // throw once! - return null; // never reached - } + /// + public void Clear() => _items.Clear(); - /// - public IEnumerable SearchByKey(string keyStartsWith) - { - return _items - .Where(kvp => kvp.Key.InvariantStartsWith(keyStartsWith)) - .Select(kvp => SafeLazy.GetSafeLazyValue(kvp.Value)) - .Where(x => x != null); - } + /// + public void Clear(string key) => _items.TryRemove(key, out _); - /// - public IEnumerable SearchByRegex(string regex) + /// + public void ClearOfType(Type? type) + { + if (type == null) { - 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); + return; } - /// - public void Clear() - { - _items.Clear(); - } + var isInterface = type.IsInterface; - /// - public void Clear(string key) - { - _items.TryRemove(key, out _); - } + 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); - /// - public void ClearOfType(Type type) + // 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); + })) { - 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 _); + _items.TryRemove(kvp.Key, out _); } + } - /// - public void ClearOfType() + /// + 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 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 _); + _items.TryRemove(kvp.Key, out _); } + } - /// - public void ClearOfType(Func predicate) + /// + 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); + })) { - 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 _); + _items.TryRemove(kvp.Key, out _); } + } - /// - public void ClearByKey(string keyStartsWith) + /// + public void ClearByKey(string keyStartsWith) + { + foreach (KeyValuePair> ikvp in _items + .Where(kvp => kvp.Key.InvariantStartsWith(keyStartsWith))) { - foreach (var ikvp in _items - .Where(kvp => kvp.Key.InvariantStartsWith(keyStartsWith))) - _items.TryRemove(ikvp.Key, out _); + _items.TryRemove(ikvp.Key, out _); } + } - /// - public void ClearByRegex(string regex) + /// + public void ClearByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + foreach (KeyValuePair> ikvp in _items + .Where(kvp => compiled.IsMatch(kvp.Key))) { - 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 e0bbd5739794..967d5aa5a70b 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"; + // prefix cache keys so we know which one are ours + protected const string CacheItemPrefix = "umbrtmche"; - #region IAppCache + #region IAppCache - /// - public virtual object? Get(string key) + /// + 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); + 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) + /// + public virtual IEnumerable SearchByKey(string keyStartsWith) + { + var plen = CacheItemPrefix.Length + 1; + IEnumerable> entries; + try { - 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(); - } + 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 + } - 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 IEnumerable SearchByRegex(string regex) + /// + public virtual void Clear() + { + try { - const string prefix = CacheItemPrefix + "-"; - var compiled = new Regex(regex, RegexOptions.Compiled); - var plen = prefix.Length; - IEnumerable> entries; - try + EnterWriteLock(); + foreach (KeyValuePair entry in GetDictionaryEntries().ToArray()) { - EnterReadLock(); - entries = GetDictionaryEntries() - .Where(x => compiled.IsMatch(((string)x.Key).Substring(plen))) - .ToArray(); // evaluate while locked + RemoveEntry((string)entry.Key); } - 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 } + finally + { + ExitWriteLock(); + } + } - /// - public virtual void Clear() + /// + public virtual void Clear(string key) + { + var cacheKey = GetCacheKey(key); + try { - try - { - EnterWriteLock(); - foreach (var entry in GetDictionaryEntries().ToArray()) - { - RemoveEntry((string) entry.Key); - } - } - finally - { - ExitWriteLock(); - } + EnterWriteLock(); + RemoveEntry(cacheKey); } + finally + { + ExitWriteLock(); + } + } - /// - public virtual void Clear(string key) + /// + public virtual void ClearOfType(Type? type) + { + if (type == null) { - var cacheKey = GetCacheKey(key); - try - { - EnterWriteLock(); - RemoveEntry(cacheKey); - } - finally - { - ExitWriteLock(); - } + return; } - /// - public virtual void ClearOfType(Type type) + var isInterface = type.IsInterface; + try { - 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 + 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()) { - ExitWriteLock(); + RemoveEntry((string)entry.Key); } } + finally + { + ExitWriteLock(); + } + } - /// - public virtual void ClearOfType() + /// + public virtual void ClearOfType() + { + Type typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; + try { - 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 + 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()) { - ExitWriteLock(); + RemoveEntry((string)entry.Key); } } + finally + { + ExitWriteLock(); + } + } - /// - public virtual void ClearOfType(Func predicate) + /// + public virtual void ClearOfType(Func predicate) + { + Type typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; + var plen = CacheItemPrefix.Length + 1; + try { - 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 + 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); + })) { - ExitWriteLock(); + RemoveEntry((string)entry.Key); } } + finally + { + ExitWriteLock(); + } + } - /// - public virtual void ClearByKey(string keyStartsWith) + /// + public virtual void ClearByKey(string keyStartsWith) + { + var plen = CacheItemPrefix.Length + 1; + try { - 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 + EnterWriteLock(); + foreach (KeyValuePair entry in GetDictionaryEntries() + .Where(x => ((string)x.Key).Substring(plen).InvariantStartsWith(keyStartsWith)) + .ToArray()) { - ExitWriteLock(); + RemoveEntry((string)entry.Key); } } + finally + { + ExitWriteLock(); + } + } - /// - public virtual void ClearByRegex(string regex) + /// + public virtual void ClearByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + var plen = CacheItemPrefix.Length + 1; + try { - 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()) { - EnterWriteLock(); - foreach (var entry in GetDictionaryEntries() - .Where(x => compiled.IsMatch(((string)x.Key).Substring(plen))) - .ToArray()) - { - RemoveEntry((string) entry.Key); - } - } - finally - { - ExitWriteLock(); + RemoveEntry((string)entry.Key); } } + finally + { + ExitWriteLock(); + } + } - #endregion + #endregion - #region Dictionary + #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); + // 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(); - // read-write lock the underlying cache - //protected abstract IDisposable ReadLock { get; } - //protected abstract IDisposable WriteLock { get; } + protected abstract void RemoveEntry(string key); - protected abstract void EnterReadLock(); - protected abstract void ExitReadLock(); - protected abstract void EnterWriteLock(); - protected abstract void ExitWriteLock(); + protected abstract object? GetEntry(string key); - protected string GetCacheKey(string key) => $"{CacheItemPrefix}-{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(); - #endregion - } + 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 81cfc2e114a6..187ff6fc1134 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 ec59bf390b28..1d0044c057bc 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 97a3bf08eb0b..dba0cd3b3fa4 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 +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); +} + +/// +/// 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 { - /// - /// 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); - } + 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 04b91e43d8b6..35eb7a279c4c 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 619fc1eb56ce..d01bf617fd61 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 21dfdd840d0f..426481ea0a08 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 af44f2c08540..4352f9be31df 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 02f37e6ea999..f88bc3bb249c 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 f283d730b511..790907c7500a 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 7c273c913611..31dc6fe09573 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)); + /// + /// Gets a cache. + /// + public IAppPolicyCache GetOrCreate() + => GetOrCreate(typeof(T)); - /// - /// Tries to get a cache. - /// - public Attempt Get() - => Get(typeof(T)); + /// + /// Tries to get a cache. + /// + public Attempt Get() + => Get(typeof(T)); - /// - /// Clears a cache. - /// - public void ClearCache() - => ClearCache(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 a6b705ae5d43..b22cff56d207 100644 --- a/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs @@ -3,58 +3,46 @@ 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 . + /// + 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 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; } - - /// - /// 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; - } - - /// - /// 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); - } - - - public string Serialize(params TJsonPayload[] jsonPayloads) - { - return JsonSerializer.Serialize(jsonPayloads); - } - #endregion - - } + /// 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) => JsonSerializer.Deserialize(json); + + 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 414c51c186ad..2ff447246b2d 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.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) - { - _publishedSnapshotService = publishedSnapshotService; - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("3E0F95D8-0BE5-44B8-8394-2B8750B62654"); - private readonly IPublishedSnapshotService _publishedSnapshotService; + ClearAllIsolatedCacheByEntityType(); - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Language Cache Refresher"; - - #endregion + // 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[] + { + new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll), + }; + _publishedSnapshotService.Notify(payloads); + } - #region Refresher + #region Json - public override void Refresh(JsonPayload[] payloads) + public class JsonPayload + { + public enum LanguageChangeType { - if (payloads.Length == 0) return; + /// + /// 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, + } - var clearDictionary = false; - var clearContent = false; + public JsonPayload(int id, string isoCode, LanguageChangeType changeType) + { + Id = id; + IsoCode = isoCode; + ChangeType = changeType; + } - //clear all no matter what type of payload - ClearAllIsolatedCacheByEntityType(); + public int Id { get; } - 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; - } - } + public string IsoCode { get; } - 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); - } + public LanguageChangeType ChangeType { get; } + } - // then trigger event - base.Refresh(payloads); - } + #endregion - // these events should never trigger - // everything should be PAYLOAD/JSON + #region Define - public override void RefreshAll() => throw new NotSupportedException(); + public static readonly Guid UniqueId = Guid.Parse("3E0F95D8-0BE5-44B8-8394-2B8750B62654"); + private readonly IPublishedSnapshotService _publishedSnapshotService; - public override void Refresh(int id) => throw new NotSupportedException(); + public override Guid RefresherUniqueId => UniqueId; - public override void Refresh(Guid id) => throw new NotSupportedException(); + public override string Name => "Language Cache Refresher"; - public override void Remove(int id) => throw new NotSupportedException(); + #endregion - #endregion + #region Refresher - /// - /// Clears all domain caches - /// - private void RefreshDomains() + public override void Refresh(JsonPayload[] payloads) + { + if (payloads.Length == 0) { - 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); + return; } - #region Json + var clearDictionary = false; + var clearContent = false; + + // clear all no matter what type of payload + ClearAllIsolatedCacheByEntityType(); - public class JsonPayload + foreach (JsonPayload payload in payloads) { - public JsonPayload(int id, string isoCode, LanguageChangeType changeType) + switch (payload.ChangeType) { - Id = id; - IsoCode = isoCode; - ChangeType = changeType; + case LanguageChangeType.Update: + clearDictionary = true; + break; + case LanguageChangeType.Remove: + case LanguageChangeType.ChangeCulture: + clearDictionary = true; + clearContent = true; + break; } + } - public int Id { get; } - public string IsoCode { get; } - public LanguageChangeType ChangeType { get; } + if (clearDictionary) + { + ClearAllIsolatedCacheByEntityType(); + } - 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 - } + // 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); } - #endregion + // 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 8f49ce134ce5..9975abae8c72 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 Json + public class JsonPayload + { + public JsonPayload(int id, string alias) + { + Id = id; + Alias = alias; } - #region Define + public int Id { get; } - public static readonly Guid UniqueId = Guid.Parse("7B1E683C-5F34-43dd-803D-9699EA1E98CA"); + public string Alias { get; } + } - public override Guid RefresherUniqueId => UniqueId; + #endregion - public override string Name => "Macro Cache Refresher"; + #region Define - #endregion + public static readonly Guid UniqueId = Guid.Parse("7B1E683C-5F34-43dd-803D-9699EA1E98CA"); - #region Refresher + public override Guid RefresherUniqueId => UniqueId; - public override void RefreshAll() - { - foreach (var prefix in GetAllMacroCacheKeys()) - AppCaches.RuntimeCache.ClearByKey(prefix); + public override string Name => "Macro Cache Refresher"; - ClearAllIsolatedCacheByEntityType(); + #endregion - base.RefreshAll(); - } + #region Refresher - public override void Refresh(string json) + public override void RefreshAll() + { + foreach (var prefix in GetAllMacroCacheKeys()) { - var payloads = Deserialize(json); - - if (payloads is not null) - { - Refresh(payloads); - } + AppCaches.RuntimeCache.ClearByKey(prefix); } - 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 - } - } + ClearAllIsolatedCacheByEntityType(); - base.Refresh(payloads); - } + base.RefreshAll(); + } - #endregion + public override void Refresh(string json) + { + JsonPayload[]? payloads = Deserialize(json); - #region Json + if (payloads is not null) + { + Refresh(payloads); + } + } - public class JsonPayload + public override void Refresh(JsonPayload[] payloads) + { + foreach (JsonPayload payload in payloads) { - public JsonPayload(int id, string alias) + foreach (var alias in GetCacheKeysForAlias(payload.Alias)) { - Id = id; - Alias = alias; + AppCaches.RuntimeCache.ClearByKey(alias); } - public int Id { get; } - - public string Alias { get; } + 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 + } } - #endregion + base.Refresh(payloads); + } - #region Helpers + #endregion - internal static string[] GetAllMacroCacheKeys() - { - return new[] - { - CacheKeys.MacroContentCacheKey, // macro render cache - CacheKeys.MacroFromAliasCacheKey, // lookup macro by alias - }; - } + #region Helpers - internal static string[] GetCacheKeysForAlias(string alias) + internal static string[] GetAllMacroCacheKeys() => + new[] { - return GetAllMacroCacheKeys().Select(x => x + alias).ToArray(); - } + CacheKeys.MacroContentCacheKey, // macro render cache + CacheKeys.MacroFromAliasCacheKey, // lookup macro by alias + }; - #endregion - } + 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 2efd23d71f4b..43e6a7ce47a1 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.Changes; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class MediaCacheRefresher : PayloadCacheRefresherBase { - 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) { - private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly IIdKeyMap _idKeyMap; + _publishedSnapshotService = publishedSnapshotService; + _idKeyMap = idKeyMap; + } - 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 - #region Define + public static void RefreshMediaTypes(AppCaches appCaches) => appCaches.IsolatedCaches.ClearCache(); - public static readonly Guid UniqueId = Guid.Parse("B29286DD-2D40-4DDB-B325-681226589FEC"); + #endregion - public override Guid RefresherUniqueId => UniqueId; + #region Json - public override string Name => "Media Cache Refresher"; + public class JsonPayload + { + public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) + { + Id = id; + Key = key; + ChangeTypes = changeTypes; + } - #endregion + public int Id { get; } - #region Refresher + public Guid? Key { get; } - public override void Refresh(JsonPayload[] payloads) - { - if (payloads == null) return; + public TreeChangeTypes ChangeTypes { get; } + } - _publishedSnapshotService.Notify(payloads, out var anythingChanged); + #endregion - if (anythingChanged) - { - AppCaches.ClearPartialViewCache(); - AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey); + #region Define - var mediaCache = AppCaches.IsolatedCaches.Get(); + public static readonly Guid UniqueId = Guid.Parse("B29286DD-2D40-4DDB-B325-681226589FEC"); - foreach (var payload in payloads) - { - if (payload.ChangeTypes == TreeChangeTypes.Remove) - _idKeyMap.ClearCache(payload.Id); - - 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)); - - // 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); - } - } - } + public override Guid RefresherUniqueId => UniqueId; - base.Refresh(payloads); - } + public override string Name => "Media Cache Refresher"; - // these events should never trigger - // everything should be JSON + #endregion - public override void RefreshAll() - { - throw new NotSupportedException(); - } + #region Refresher - public override void Refresh(int id) + public override void Refresh(JsonPayload[]? payloads) + { + if (payloads == null) { - throw new NotSupportedException(); + return; } - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - } + _publishedSnapshotService.Notify(payloads, out var anythingChanged); - public override void Remove(int id) + if (anythingChanged) { - throw new NotSupportedException(); - } - - #endregion + AppCaches.ClearPartialViewCache(); + AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey); - #region Json + Attempt mediaCache = AppCaches.IsolatedCaches.Get(); - public class JsonPayload - { - public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) + foreach (JsonPayload payload in payloads) { - Id = id; - Key = key; - ChangeTypes = changeTypes; - } - - public int Id { get; } - public Guid? Key { get; } - public TreeChangeTypes ChangeTypes { get; } - } + if (payload.ChangeTypes == TreeChangeTypes.Remove) + { + _idKeyMap.ClearCache(payload.Id); + } - #endregion + if (!mediaCache.Success) + { + continue; + } - #region Indirect + // 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)); - public static void RefreshMediaTypes(AppCaches appCaches) - { - appCaches.IsolatedCaches.ClearCache(); + // 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); + } + } } - #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 9869f226b9ed..ac9dac5a09d1 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.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache -{ - public sealed class MemberCacheRefresher : PayloadCacheRefresherBase - { - private readonly IIdKeyMap _idKeyMap; - - public MemberCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) - { - _idKeyMap = idKeyMap; - } - - public class JsonPayload - { - //[JsonConstructor] - public JsonPayload(int id, string? username, bool removed) - { - Id = id; - Username = username; - Removed = removed; - } +namespace Umbraco.Cms.Core.Cache; - public int Id { get; } - public string? Username { get; } - public bool Removed { get; } - } - - #region Define +public sealed class MemberCacheRefresher : PayloadCacheRefresherBase +{ + public static readonly Guid UniqueId = Guid.Parse("E285DF34-ACDC-4226-AE32-C0CB5CF388DA"); - public static readonly Guid UniqueId = Guid.Parse("E285DF34-ACDC-4226-AE32-C0CB5CF388DA"); + private readonly IIdKeyMap _idKeyMap; - public override Guid RefresherUniqueId => UniqueId; + public MemberCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) => + _idKeyMap = idKeyMap; - public override string Name => "Member Cache Refresher"; + #region Indirect - #endregion + public static void RefreshMemberTypes(AppCaches appCaches) => appCaches.IsolatedCaches.ClearCache(); - #region Refresher + #endregion - public override void Refresh(JsonPayload[] payloads) + public class JsonPayload + { + // [JsonConstructor] + public JsonPayload(int id, string? username, bool removed) { - ClearCache(payloads); - base.Refresh(payloads); + Id = id; + Username = username; + Removed = removed; } - public override void Refresh(int id) - { - ClearCache(new JsonPayload(id, null, false)); - base.Refresh(id); - } + public int Id { get; } - public override void Remove(int id) - { - ClearCache(new JsonPayload(id, null, false)); - base.Remove(id); - } + public string? Username { get; } - private void ClearCache(params JsonPayload[] payloads) - { - AppCaches.ClearPartialViewCache(); - var memberCache = AppCaches.IsolatedCaches.Get(); + public bool Removed { 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)); - } - } + public override Guid RefresherUniqueId => UniqueId; - } + public override string Name => "Member Cache Refresher"; - #endregion + public override void Refresh(JsonPayload[] payloads) + { + ClearCache(payloads); + base.Refresh(payloads); + } - #region Indirect + public override void Refresh(int id) + { + ClearCache(new JsonPayload(id, null, false)); + base.Refresh(id); + } - public static void RefreshMemberTypes(AppCaches appCaches) + 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) { - appCaches.IsolatedCaches.ClearCache(); + _idKeyMap.ClearCache(p.Id); + if (memberCache.Success) + { + memberCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Id)); + memberCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Username)); + } } - - #endregion } } diff --git a/src/Umbraco.Core/Cache/MemberGroupCacheRefresher.cs b/src/Umbraco.Core/Cache/MemberGroupCacheRefresher.cs index 0866f7b39aff..05bd6049c8e9 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 Json + + public class JsonPayload + { + public JsonPayload(int id, string name) + { + Id = id; + Name = name; } - #region Define + public string Name { get; } - public static readonly Guid UniqueId = Guid.Parse("187F236B-BD21-4C85-8A7C-29FBA3D6C00C"); + public int Id { get; } + } - public override Guid RefresherUniqueId => UniqueId; + #endregion - public override string Name => "Member Group Cache Refresher"; + #region Define - #endregion + public static readonly Guid UniqueId = Guid.Parse("187F236B-BD21-4C85-8A7C-29FBA3D6C00C"); - #region Refresher + public override Guid RefresherUniqueId => UniqueId; - public override void Refresh(string json) - { - ClearCache(); - base.Refresh(json); - } + public override string Name => "Member Group Cache Refresher"; - public override void Refresh(int id) - { - ClearCache(); - base.Refresh(id); - } + #endregion - public override void Remove(int id) - { - ClearCache(); - base.Remove(id); - } + #region Refresher - 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(); - } + public override void Refresh(string json) + { + ClearCache(); + base.Refresh(json); + } - #endregion + public override void Refresh(int id) + { + ClearCache(); + base.Refresh(id); + } - #region Json + public override void Remove(int id) + { + ClearCache(); + base.Remove(id); + } - public class JsonPayload - { - public JsonPayload(int id, string name) - { - Id = id; - Name = name; - } - - public string Name { get; } - public int Id { get; } - } + 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 - } + #endregion } diff --git a/src/Umbraco.Core/Cache/NoAppCache.cs b/src/Umbraco.Core/Cache/NoAppCache.cs index ef22a51ab091..70edbcf61d79 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(); + /// + /// Gets the singleton instance. + /// + public static NoAppCache Instance { get; } = new(); - /// - public bool IsAvailable => false; + /// + public bool IsAvailable => false; - /// - public virtual object? Get(string cacheKey) - { - return null; - } + /// + public virtual object? Get(string cacheKey) => null; - /// - public virtual object? Get(string cacheKey, Func factory) - { - return factory(); - } + /// + public virtual object? Get(string cacheKey, Func factory) => factory(); - public bool Set(string key, object? value) => false; + /// + public virtual IEnumerable SearchByKey(string keyStartsWith) => Enumerable.Empty(); - public bool Remove(string key) => false; + /// + public IEnumerable SearchByRegex(string regex) => Enumerable.Empty(); - /// - public virtual IEnumerable SearchByKey(string keyStartsWith) - { - return Enumerable.Empty(); - } + /// + public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) => factory(); - /// - public IEnumerable SearchByRegex(string regex) - { - return Enumerable.Empty(); - } + /// + public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) + { + } - /// - public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) - { - return factory(); - } + /// + public virtual void Clear() + { + } - /// - public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) - { } + /// + public virtual void Clear(string key) + { + } - /// - public virtual void Clear() - { } + /// + public virtual void ClearOfType(Type type) + { + } - /// - public virtual void Clear(string key) - { } + /// + public virtual void ClearOfType() + { + } - /// - public virtual void ClearOfType(Type type) - { } + /// + public virtual void ClearOfType(Func predicate) + { + } - /// - public virtual void ClearOfType() - { } + /// + public virtual void ClearByKey(string keyStartsWith) + { + } - /// - public virtual void ClearOfType(Func predicate) - { } + /// + public virtual void ClearByRegex(string regex) + { + } - /// - public virtual void ClearByKey(string keyStartsWith) - { } + public bool Set(string key, object? value) => false; - /// - public virtual void ClearByRegex(string regex) - { } + public bool Remove(string key) => false; - public IEnumerator> GetEnumerator() => new Dictionary().GetEnumerator(); + public IEnumerator> GetEnumerator() => + new Dictionary().GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/Umbraco.Core/Cache/NoCacheRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/NoCacheRepositoryCachePolicy.cs index b99975e0e481..2b662d4c2cde 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() + { + } + + public static NoCacheRepositoryCachePolicy Instance { get; } = new(); + + public TEntity? Get(TId? id, Func performGet, Func?> performGetAll) => + performGet(id); + + public TEntity? GetCached(TId id) => null; + + public bool Exists(TId id, Func performExists, Func?> performGetAll) => + performExists(id); + + public void Create(TEntity entity, Action persistNew) => persistNew(entity); + + public void Update(TEntity entity, Action persistUpdated) => persistUpdated(entity); + + public void Delete(TEntity entity, Action persistDeleted) => persistDeleted(entity); + + public TEntity[] GetAll(TId[]? ids, Func?> performGetAll) => + performGetAll(ids)?.ToArray() ?? Array.Empty(); + + public void ClearAll() { - private NoCacheRepositoryCachePolicy() { } - - public static NoCacheRepositoryCachePolicy Instance { get; } = new NoCacheRepositoryCachePolicy(); - - public TEntity? Get(TId? id, Func performGet, Func?> performGetAll) - { - return performGet(id); - } - - public TEntity? GetCached(TId id) - { - return null; - } - - public bool Exists(TId id, Func performExists, Func?> performGetAll) - { - return performExists(id); - } - - public void Create(TEntity entity, Action persistNew) - { - persistNew(entity); - } - - public void Update(TEntity entity, Action persistUpdated) - { - persistUpdated(entity); - } - - public void Delete(TEntity entity, Action persistDeleted) - { - persistDeleted(entity); - } - - public TEntity[] GetAll(TId[]? ids, Func?> performGetAll) - { - return performGetAll(ids)?.ToArray() ?? Array.Empty(); - } - - public void ClearAll() - { } } } diff --git a/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs b/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs index 4ec91c4933ea..dcd83ece94d5 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 +namespace Umbraco.Cms.Core.Cache; + +/// +/// Implements on top of a . +/// +public class ObjectCacheAppCache : IAppPolicyCache, IDisposable { + private readonly ReaderWriterLockSlim _locker = new(LockRecursionPolicy.SupportsRecursion); + private bool _disposedValue; + /// - /// Implements on top of a . + /// Initializes a new instance of the . /// - public class ObjectCacheAppCache : IAppPolicyCache, IDisposable - { - private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - private bool _disposedValue; + public ObjectCacheAppCache() => - /// - /// 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"); - } + // 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; } + /// + /// Gets the internal memory cache, for tests only! + /// + public ObjectCache MemoryCache { get; private set; } - /// - public object? Get(string key) + /// + public object? Get(string key) + { + Lazy? result; + try { - Lazy? result; - try - { - _locker.EnterReadLock(); - result = MemoryCache.Get(key) as Lazy; // null if key not found - } - finally + _locker.EnterReadLock(); + result = MemoryCache.Get(key) as Lazy; // null if key not found + } + finally + { + if (_locker.IsReadLockHeld) { - if (_locker.IsReadLockHeld) - _locker.ExitReadLock(); + _locker.ExitReadLock(); } - return result == null ? null : SafeLazy.GetSafeLazyValue(result); // return exceptions as null } - /// - public object? Get(string key, Func factory) + 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 { - return Get(key, factory, null); + _locker.EnterReadLock(); + entries = MemoryCache + .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) + .ToArray(); // evaluate while locked } - - /// - public IEnumerable SearchByKey(string keyStartsWith) + finally { - KeyValuePair[] entries; - try + if (_locker.IsReadLockHeld) { - _locker.EnterReadLock(); - entries = MemoryCache - .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) - .ToArray(); // evaluate while locked + _locker.ExitReadLock(); } - 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); + 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()!; + } - KeyValuePair[] entries; - try - { - _locker.EnterReadLock(); - entries = MemoryCache - .Where(x => compiled.IsMatch(x.Key)) - .ToArray(); // evaluate while locked - } - finally + /// + 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) { - if (_locker.IsReadLockHeld) - _locker.ExitReadLock(); + _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) + 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 { - // see notes in HttpRuntimeAppCache + _locker.EnterUpgradeableReadLock(); - Lazy? result; + result = MemoryCache.Get(key) as Lazy; - try + // get non-created as NonCreatedValue & exceptions as null + if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) { - _locker.EnterUpgradeableReadLock(); + result = SafeLazy.GetSafeLazy(factory); + CacheItemPolicy policy = GetPolicy(timeout, isSliding, dependentFiles); - result = MemoryCache.Get(key) as Lazy; - if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null + try { - result = SafeLazy.GetSafeLazy(factory); - var policy = GetPolicy(timeout, isSliding, dependentFiles); + _locker.EnterWriteLock(); - try - { - _locker.EnterWriteLock(); - //NOTE: This does an add or update - MemoryCache.Set(key, result, policy); - } - finally + // NOTE: This does an add or update + MemoryCache.Set(key, result, policy); + } + finally + { + if (_locker.IsWriteLockHeld) { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); + _locker.ExitWriteLock(); } } } - finally + } + finally + { + if (_locker.IsUpgradeableReadLockHeld) { - if (_locker.IsUpgradeableReadLockHeld) - _locker.ExitUpgradeableReadLock(); + _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) + // return result.Value; + var value = result.Value; // will not throw (safe lazy) + if (value is SafeLazy.ExceptionHolder eh) { - // 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. + eh.Exception.Throw(); // throw once! + } - var result = SafeLazy.GetSafeLazy(factory); - var value = result.Value; // force evaluation now - if (value == null) return; // do not store null values (backward compat) + return value; + } - var policy = GetPolicy(timeout, isSliding, dependentFiles); - //NOTE: This does an add or update - MemoryCache.Set(key, result, policy); + /// + 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) } - /// - public virtual void Clear() + CacheItemPolicy policy = GetPolicy(timeout, isSliding, dependentFiles); + + // NOTE: This does an add or update + MemoryCache.Set(key, result, policy); + } + + /// + public virtual void Clear() + { + try { - try - { - _locker.EnterWriteLock(); - MemoryCache.DisposeIfDisposable(); - MemoryCache = new MemoryCache("in-memory"); - } - finally + _locker.EnterWriteLock(); + MemoryCache.DisposeIfDisposable(); + MemoryCache = new MemoryCache("in-memory"); + } + finally + { + if (_locker.IsWriteLockHeld) { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); + _locker.ExitWriteLock(); } } + } - /// - public virtual void Clear(string key) + /// + public virtual void Clear(string key) + { + try { - try + _locker.EnterWriteLock(); + if (MemoryCache[key] == null) { - _locker.EnterWriteLock(); - if (MemoryCache[key] == null) return; - MemoryCache.Remove(key); + return; } - finally + + MemoryCache.Remove(key); + } + finally + { + if (_locker.IsWriteLockHeld) { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); + _locker.ExitWriteLock(); } } + } - /// - public virtual void ClearOfType(Type type) + /// + public virtual void ClearOfType(Type type) + { + if (type == null) { - 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(); - } + return; } - /// - public virtual void ClearOfType() + var isInterface = type.IsInterface; + try { - 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()) { - _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); + MemoryCache.Remove(key); } - finally + } + finally + { + if (_locker.IsWriteLockHeld) { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); + _locker.ExitWriteLock(); } } + } - /// - public virtual void ClearOfType(Func predicate) + /// + public virtual void ClearOfType() + { + try { - 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()) { - _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); + MemoryCache.Remove(key); } - finally + } + finally + { + if (_locker.IsWriteLockHeld) { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); + _locker.ExitWriteLock(); } } + } - /// - public virtual void ClearByKey(string keyStartsWith) + /// + public virtual void ClearOfType(Func predicate) + { + try { - 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()) { - _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); + MemoryCache.Remove(key); } - finally + } + finally + { + if (_locker.IsWriteLockHeld) { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); + _locker.ExitWriteLock(); } } + } - /// - public virtual void ClearByRegex(string regex) + /// + public virtual void ClearByKey(string keyStartsWith) + { + try { - var compiled = new Regex(regex, RegexOptions.Compiled); + _locker.EnterWriteLock(); - try + // ToArray required to remove + foreach (var key in MemoryCache + .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) + .Select(x => x.Key) + .ToArray()) { - _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); + MemoryCache.Remove(key); } - finally + } + finally + { + if (_locker.IsWriteLockHeld) { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); + _locker.ExitWriteLock(); } } + } - private static CacheItemPolicy GetPolicy(TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) + /// + public virtual void ClearByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + + try { - var absolute = isSliding ? ObjectCache.InfiniteAbsoluteExpiration : (timeout == null ? ObjectCache.InfiniteAbsoluteExpiration : DateTime.Now.Add(timeout.Value)); - var sliding = isSliding == false ? ObjectCache.NoSlidingExpiration : (timeout ?? ObjectCache.NoSlidingExpiration); + _locker.EnterWriteLock(); - var policy = new CacheItemPolicy + // ToArray required to remove + foreach (var key in MemoryCache + .Where(x => compiled.IsMatch(x.Key)) + .Select(x => x.Key) + .ToArray()) { - AbsoluteExpiration = absolute, - SlidingExpiration = sliding - }; - - if (dependentFiles != null && dependentFiles.Any()) + MemoryCache.Remove(key); + } + } + finally + { + if (_locker.IsWriteLockHeld) { - policy.ChangeMonitors.Add(new HostFileChangeMonitor(dependentFiles.ToList())); + _locker.ExitWriteLock(); } - - return policy; } + } + + public void Dispose() => + + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - if (!_disposedValue) + if (disposing) { - if (disposing) - { - _locker.Dispose(); - } - _disposedValue = true; + _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 }; - public void Dispose() + if (dependentFiles != null && dependentFiles.Any()) { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + 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 2dc3ddcf1bb7..f371e809793b 100644 --- a/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs @@ -3,49 +3,48 @@ 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 + #region Refresher - public override void Refresh(string json) + public override void Refresh(string json) + { + TPayload[]? payload = Deserialize(json); + if (payload is not null) { - var payload = Deserialize(json); - if (payload is not null) - { - Refresh(payload); - } + Refresh(payload); } + } - /// - /// Refreshes as specified by a payload. - /// - /// The payload. - public virtual void Refresh(TPayload[] payloads) - { - OnCacheUpdated(NotificationFactory.Create(payloads, MessageType.RefreshByPayload)); - } + /// + /// Refreshes as specified by a payload. + /// + /// The payload. + public virtual void Refresh(TPayload[] payloads) => + OnCacheUpdated(NotificationFactory.Create(payloads, MessageType.RefreshByPayload)); - #endregion - } + #endregion } diff --git a/src/Umbraco.Core/Cache/PublicAccessCacheRefresher.cs b/src/Umbraco.Core/Cache/PublicAccessCacheRefresher.cs index 5c9eb20b4c53..9124d7350ec3 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 -{ - public sealed class PublicAccessCacheRefresher : CacheRefresherBase - { - public PublicAccessCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { } +namespace Umbraco.Cms.Core.Cache; - #region Define +public sealed class PublicAccessCacheRefresher : CacheRefresherBase +{ + #region Define - public static readonly Guid UniqueId = Guid.Parse("1DB08769-B104-4F8B-850E-169CAC1DF2EC"); + public static readonly Guid UniqueId = Guid.Parse("1DB08769-B104-4F8B-850E-169CAC1DF2EC"); - public override Guid RefresherUniqueId => UniqueId; + public PublicAccessCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) + { + } - public override string Name => "Public Access Cache Refresher"; + public override Guid RefresherUniqueId => UniqueId; - #endregion + public override string Name => "Public Access Cache Refresher"; - #region Refresher + #endregion - public override void Refresh(Guid id) - { - ClearAllIsolatedCacheByEntityType(); - base.Refresh(id); - } + #region Refresher - public override void Refresh(int id) - { - ClearAllIsolatedCacheByEntityType(); - base.Refresh(id); - } + public override void Refresh(Guid id) + { + ClearAllIsolatedCacheByEntityType(); + base.Refresh(id); + } - public override void RefreshAll() - { - ClearAllIsolatedCacheByEntityType(); - base.RefreshAll(); - } + public override void Refresh(int id) + { + ClearAllIsolatedCacheByEntityType(); + base.Refresh(id); + } - public override void Remove(int id) - { - ClearAllIsolatedCacheByEntityType(); - base.Remove(id); - } + public override void RefreshAll() + { + ClearAllIsolatedCacheByEntityType(); + base.RefreshAll(); + } - #endregion + 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 9f1c45374e65..8da3cd5be0a4 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 override Guid RefresherUniqueId => UniqueId; + public static readonly Guid UniqueId = Guid.Parse("D8375ABA-4FB3-4F86-B505-92FBA1B6F7C9"); - public override string Name => "Relation Type Cache Refresher"; + public override Guid RefresherUniqueId => UniqueId; - #endregion + public override string Name => "Relation Type Cache Refresher"; - #region Refresher + public override void RefreshAll() + { + ClearAllIsolatedCacheByEntityType(); + base.RefreshAll(); + } - 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) - { - var cache = AppCaches.IsolatedCaches.Get(); - if (cache.Success) cache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); - base.Refresh(id); - } + base.Refresh(id); + } - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - //base.Refresh(id); - } + public override void Refresh(Guid id) => throw new NotSupportedException(); - public override void Remove(int id) + // 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.Remove(id); + cache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); } - #endregion + base.Remove(id); } } diff --git a/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs index c719ce72e5f4..ba7b251aa0fc 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; - } + PerformCount = performCount; + GetAllCacheValidateCount = true; + GetAllCacheAllowZeroCount = false; + } - /// - /// Ctor - sets GetAllCacheValidateCount = false - /// - public RepositoryCachePolicyOptions() - { - PerformCount = null; - GetAllCacheValidateCount = false; - 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; } + /// + /// 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/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; } - } + /// + /// 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 387e5c0271f3..40512ece67ac 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) - { - // 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 Lazy GetSafeLazy(Func getCacheItem) => - public static object? GetSafeLazyValue(Lazy? lazy, bool onlyIfValueIsCreated = false) + // 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(() => { - // 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 0bc2c6c5ef1c..221ad7c8363a 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; - - public TemplateCacheRefresher(AppCaches appCaches, IIdKeyMap idKeyMap, IContentTypeCommonRepository contentTypeCommonRepository, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { - _idKeyMap = idKeyMap; - _contentTypeCommonRepository = contentTypeCommonRepository; - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("DD12B6A0-14B9-46e8-8800-C154F74047C8"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Template Cache Refresher"; - - #endregion + _idKeyMap = idKeyMap; + _contentTypeCommonRepository = contentTypeCommonRepository; + } - #region Refresher + public override Guid RefresherUniqueId => UniqueId; - public override void Refresh(int id) - { - RemoveFromCache(id); - base.Refresh(id); - } + public override string Name => "Template Cache Refresher"; - public override void Remove(int id) - { - RemoveFromCache(id); + public override void Refresh(int id) + { + RemoveFromCache(id); + base.Refresh(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(); + public override void Remove(int id) + { + RemoveFromCache(id); - base.Remove(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(); - private void RemoveFromCache(int id) - { - _idKeyMap.ClearCache(id); - AppCaches.RuntimeCache.Clear($"{CacheKeys.TemplateFrontEndCacheKey}{id}"); + base.Remove(id); + } - //need to clear the runtime cache for templates - ClearAllIsolatedCacheByEntityType(); - } + private void RemoveFromCache(int id) + { + _idKeyMap.ClearCache(id); + AppCaches.RuntimeCache.Clear($"{CacheKeys.TemplateFrontEndCacheKey}{id}"); - #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 10c4865ba828..d1dc194f9b20 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 - { - public UserCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { } + #region Define - #region Define + public static readonly Guid UniqueId = Guid.Parse("E057AF6D-2EE6-41F4-8045-3694010F0AA6"); - 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 override Guid RefresherUniqueId => UniqueId; + public override Guid RefresherUniqueId => UniqueId; - public override string Name => "User Cache Refresher"; + public override string Name => "User Cache Refresher"; - #endregion + #endregion - #region Refresher + #region Refresher - public override void RefreshAll() - { - ClearAllIsolatedCacheByEntityType(); - base.RefreshAll(); - } + public override void RefreshAll() + { + ClearAllIsolatedCacheByEntityType(); + base.RefreshAll(); + } - public override void Refresh(int id) - { - Remove(id); - base.Refresh(id); - } + public override void Refresh(int id) + { + Remove(id); + base.Refresh(id); + } - public override void Remove(int id) + public override void Remove(int id) + { + Attempt userCache = AppCaches.IsolatedCaches.Get(); + if (userCache.Success) { - 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); + 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); } - #endregion + + base.Remove(id); } + + #endregion } diff --git a/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs b/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs index a889146794ad..ccf004a8d7d0 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 - { - public UserGroupCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { } + #region Define - #region Define + public static readonly Guid UniqueId = Guid.Parse("45178038-B232-4FE8-AA1A-F2B949C44762"); - 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 override Guid RefresherUniqueId => UniqueId; + public override Guid RefresherUniqueId => UniqueId; - public override string Name => "User Group Cache Refresher"; + public override string Name => "User Group Cache Refresher"; - #endregion + #endregion - #region Refresher + #region Refresher - public override void RefreshAll() + public override void RefreshAll() + { + ClearAllIsolatedCacheByEntityType(); + Attempt userGroupCache = AppCaches.IsolatedCaches.Get(); + if (userGroupCache.Success) { - 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(); + userGroupCache.Result?.ClearByKey(CacheKeys.UserGroupGetByAliasCacheKeyPrefix); } - public override void Refresh(int id) - { - Remove(id); - base.Refresh(id); - } + // We'll need to clear all user cache too + ClearAllIsolatedCacheByEntityType(); - 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); - } + base.RefreshAll(); + } - //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(); + public override void Refresh(int id) + { + Remove(id); + base.Refresh(id); + } - base.Remove(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); } - #endregion + // 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 7d5f20efb45a..358134ab14f3 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 Dictionary> _valueEditorCache; - private readonly object _dictionaryLocker; + private readonly object _dictionaryLocker; + private readonly Dictionary> _valueEditorCache; - public ValueEditorCache() - { - _valueEditorCache = new Dictionary>(); - _dictionaryLocker = new object(); - } + public ValueEditorCache() + { + _valueEditorCache = new Dictionary>(); + _dictionaryLocker = new object(); + } - public IDataValueEditor GetValueEditor(IDataEditor editor, IDataType dataType) + public IDataValueEditor GetValueEditor(IDataEditor editor, IDataType dataType) + { + // Lock just in case multiple threads uses the cache at the same time. + lock (_dictionaryLocker) { - // 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; } + + valueEditor = editor.GetValueEditor(dataType.Configuration); + _valueEditorCache[editor.Alias] = new Dictionary { [dataType.Id] = valueEditor }; + return valueEditor; } + } - public void ClearCache(IEnumerable dataTypeIds) + 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 c815ca7a7167..68ccdea20d1d 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 f6ee1217425d..12e95f1e042f 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 6c4e2b9d0499..13ec38f892c7 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) => ObjectId = new Guid(objectId); - public UmbracoObjectTypeAttribute(string objectId, Type modelType) - { - ObjectId = new Guid(objectId); - ModelType = modelType; - } + public UmbracoObjectTypeAttribute(string objectId, Type modelType) + { + ObjectId = new Guid(objectId); + ModelType = modelType; + } - public Guid ObjectId { get; private set; } + public Guid ObjectId { get; } - public Type? ModelType { get; private set; } - } + public Type? ModelType { get; } } diff --git a/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs b/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs index 5f889daa5c9b..90df3185c677 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 a9bd71c6cca2..abbde4f3f05b 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 { + private readonly int _key1; + private readonly string _key2; + /// - /// Represents a composite key of (int, string) for fast dictionaries. + /// Initializes a new instance of the struct. /// - /// - /// 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 + public CompositeIntStringKey(int? key1, string? key2) { - private readonly int _key1; - private readonly 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; + _key1 = key1 ?? -1; + _key2 = key2?.ToLowerInvariant() ?? "NULL"; + } - public override bool Equals(object? obj) - => obj is CompositeIntStringKey other && _key2 == other._key2 && _key1 == other._key1; + public static bool operator ==(CompositeIntStringKey key1, CompositeIntStringKey key2) + => key1._key2 == key2._key2 && key1._key1 == key2._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; + public bool Equals(CompositeIntStringKey other) + => _key2 == other._key2 && _key1 == other._key1; - public static bool operator !=(CompositeIntStringKey key1, CompositeIntStringKey key2) - => key1._key2 != key2._key2 || key1._key1 != key2._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 2886de92f178..0b3ec1aa929b 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"; - } + _key1 = key1?.ToLowerInvariant() ?? "NULL"; + _key2 = key2?.ToLowerInvariant() ?? "NULL"; + } - public bool Equals(CompositeNStringNStringKey other) - => _key2 == other._key2 && _key1 == other._key1; + public static bool operator ==(CompositeNStringNStringKey key1, CompositeNStringNStringKey key2) + => key1._key2 == key2._key2 && key1._key1 == key2._key1; - public override bool Equals(object? obj) - => obj is CompositeNStringNStringKey other && _key2 == other._key2 && _key1 == other._key1; + public static bool operator !=(CompositeNStringNStringKey key1, CompositeNStringNStringKey key2) + => key1._key2 != key2._key2 || key1._key1 != key2._key1; - public override int GetHashCode() - => _key2.GetHashCode() * 31 + _key1.GetHashCode(); + public bool Equals(CompositeNStringNStringKey other) + => _key2 == other._key2 && _key1 == other._key1; - public static bool operator ==(CompositeNStringNStringKey key1, CompositeNStringNStringKey key2) - => key1._key2 == key2._key2 && key1._key1 == key2._key1; + public override bool Equals(object? obj) + => obj is CompositeNStringNStringKey other && _key2 == other._key2 && _key1 == other._key1; - public static bool operator !=(CompositeNStringNStringKey key1, CompositeNStringNStringKey key2) - => key1._key2 != key2._key2 || key1._key1 != key2._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 01f94bf149e9..6fd25f6c1212 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)); - } + _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 static bool operator ==(CompositeStringStringKey key1, CompositeStringStringKey key2) + => key1._key2 == key2._key2 && key1._key1 == key2._key1; - public override bool Equals(object? obj) - => obj is CompositeStringStringKey other && _key2 == other._key2 && _key1 == other._key1; + public static bool operator !=(CompositeStringStringKey key1, CompositeStringStringKey key2) + => key1._key2 != key2._key2 || key1._key1 != key2._key1; - public override int GetHashCode() - => _key2.GetHashCode() * 31 + _key1.GetHashCode(); + public bool Equals(CompositeStringStringKey other) + => _key2 == other._key2 && _key1 == other._key1; - public static bool operator ==(CompositeStringStringKey key1, CompositeStringStringKey key2) - => key1._key2 == key2._key2 && key1._key1 == key2._key1; + public override bool Equals(object? obj) + => obj is CompositeStringStringKey other && _key2 == other._key2 && _key1 == other._key1; - public static bool operator !=(CompositeStringStringKey key1, CompositeStringStringKey key2) - => key1._key2 != key2._key2 || key1._key1 != key2._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 ea737e0522a4..ea9a5a496f42 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; - } + Type1 = type1; + Type2 = type2; + } - /// - /// Gets the first type. - /// - public Type Type1 { get; } + /// + /// Gets the first type. + /// + public Type Type1 { get; } - /// - /// Gets the second type. - /// - public Type Type2 { get; } + /// + /// Gets the second type. + /// + public Type Type2 { get; } - /// - public bool Equals(CompositeTypeTypeKey other) - { - return Type1 == other.Type1 && Type2 == other.Type2; - } + public static bool operator ==(CompositeTypeTypeKey key1, CompositeTypeTypeKey key2) => + key1.Type1 == key2.Type1 && key1.Type2 == key2.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) => + 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 bool Equals(CompositeTypeTypeKey other) => Type1 == other.Type1 && Type2 == other.Type2; - public static bool operator !=(CompositeTypeTypeKey key1, CompositeTypeTypeKey key2) - { - return key1.Type1 != key2.Type1 || key1.Type2 != key2.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() + /// + public override int GetHashCode() + { + unchecked { - 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 f9c10e560762..a79c61a17364 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) + get { try { - _instanceLocker.EnterWriteLock(); - return _innerSet.Remove(item); + _instanceLocker.EnterReadLock(); + return _innerSet.Count; } finally { - if (_instanceLocker.IsWriteLockHeld) - _instanceLocker.ExitWriteLock(); + if (_instanceLocker.IsReadLockHeld) + { + _instanceLocker.ExitReadLock(); + } } } + } + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + /// 1 + public IEnumerator GetEnumerator() => GetThreadSafeClone().GetEnumerator(); - /// - /// 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(); - } + /// + /// 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); } - - /// - /// 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) + finally { - try + if (_instanceLocker.IsWriteLockHeld) { - _instanceLocker.EnterWriteLock(); - _innerSet.Add(item); - } - finally - { - if (_instanceLocker.IsWriteLockHeld) - _instanceLocker.ExitWriteLock(); + _instanceLocker.ExitWriteLock(); } } + } - /// - /// Attempts to add an item to the collection - /// - /// - /// - public bool TryAdd(T item) - { - if (Contains(item)) return false; - try - { - _instanceLocker.EnterWriteLock(); + /// + /// Gets a value indicating whether the is read-only. + /// + /// + /// true if the is read-only; otherwise, false. + /// + public bool IsReadOnly => false; - //double check - if (_innerSet.Contains(item)) return false; - _innerSet.Add(item); - return true; - } - finally + /// + /// 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) { - if (_instanceLocker.IsWriteLockHeld) - _instanceLocker.ExitWriteLock(); + _instanceLocker.ExitWriteLock(); } } + } - /// - /// Removes all items from the . - /// - /// The is read-only. - public void Clear() + /// + /// Removes all items from the . + /// + /// + /// The is + /// read-only. + /// + public void Clear() + { + try { - try - { - _instanceLocker.EnterWriteLock(); - _innerSet.Clear(); - } - finally + _instanceLocker.EnterWriteLock(); + _innerSet.Clear(); + } + finally + { + if (_instanceLocker.IsWriteLockHeld) { - if (_instanceLocker.IsWriteLockHeld) - _instanceLocker.ExitWriteLock(); + _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) + /// + /// 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 { - try - { - _instanceLocker.EnterReadLock(); - return _innerSet.Contains(item); - } - finally + _instanceLocker.EnterReadLock(); + return _innerSet.Contains(item); + } + finally + { + if (_instanceLocker.IsReadLockHeld) { - if (_instanceLocker.IsReadLockHeld) - _instanceLocker.ExitReadLock(); + _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); + } - /// - /// 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) + /// + /// Attempts to add an item to the collection + /// + /// + /// + public bool TryAdd(T item) + { + if (Contains(item)) { - var clone = GetThreadSafeClone(); - clone.CopyTo(array, index); + return false; } - private HashSet GetThreadSafeClone() + try { - HashSet? clone = null; - try + _instanceLocker.EnterWriteLock(); + + // double check + if (_innerSet.Contains(item)) { - _instanceLocker.EnterReadLock(); - clone = new HashSet(_innerSet, _innerSet.Comparer); + return false; } - finally + + _innerSet.Add(item); + return true; + } + finally + { + if (_instanceLocker.IsWriteLockHeld) { - if (_instanceLocker.IsReadLockHeld) - _instanceLocker.ExitReadLock(); + _instanceLocker.ExitWriteLock(); } - 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) + { + HashSet clone = GetThreadSafeClone(); + Array.Copy(clone.ToArray(), 0, array, index, clone.Count); + } - /// - /// 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) + private HashSet GetThreadSafeClone() + { + HashSet? clone = null; + try + { + _instanceLocker.EnterReadLock(); + clone = new HashSet(_innerSet, _innerSet.Comparer); + } + finally { - var clone = GetThreadSafeClone(); - Array.Copy(clone.ToArray(), 0, array, index, clone.Count); + if (_instanceLocker.IsReadLockHeld) + { + _instanceLocker.ExitReadLock(); + } } + + return clone; } } diff --git a/src/Umbraco.Core/Collections/DeepCloneableList.cs b/src/Umbraco.Core/Collections/DeepCloneableList.cs index db7677153cb9..301795281c66 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) - { - } + 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() + /// + /// Creates a new list and adds each element as a deep cloned element if it is of type IDeepCloneable + /// + /// + public object DeepClone() + { + switch (_listCloneBehavior) { - 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) + 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) { - if (item is IDeepCloneable dc) - { - newList.Add((T)dc.DeepClone()); - } - else - { - newList.Add(item); - } + newList.Add((T)dc.DeepClone()); } - 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) + else { - if (item is IDeepCloneable dc) - { - newList2.Add((T)dc.DeepClone()); - } - else - { - newList2.Add(item); - } + newList.Add(item); } - return newList2; - default: - throw new ArgumentOutOfRangeException(); - } - } + } + + 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); + } + } - #region IRememberBeingDirty - public bool IsDirty() - { - return this.OfType().Any(x => x.IsDirty()); + return newList2; + default: + throw new ArgumentOutOfRangeException(); } + } - public bool WasDirty() - { - return this.OfType().Any(x => x.WasDirty()); - } + #region IRememberBeingDirty - /// - /// Always return false, the list has no properties that can be dirty. - public bool IsPropertyDirty(string propName) - { - return false; - } + public bool IsDirty() => this.OfType().Any(x => x.IsDirty()); - /// - /// Always return false, the list has no properties that can be dirty. - public bool WasPropertyDirty(string propertyName) - { - return false; - } + public bool WasDirty() => this.OfType().Any(x => x.WasDirty()); - /// - /// Always return an empty enumerable, the list has no properties that can be dirty. - public IEnumerable GetDirtyProperties() - { - return Enumerable.Empty(); - } + /// + /// Always return false, the list has no properties that can be dirty. + public bool IsPropertyDirty(string propName) => false; - public void ResetDirtyProperties() - { - foreach (var dc in this.OfType()) - { - dc.ResetDirtyProperties(); - } - } + /// + /// Always return false, the list has no properties that can be dirty. + public bool WasPropertyDirty(string propertyName) => false; - public void DisableChangeTracking() - { - // noop - } + /// + /// Always return an empty enumerable, the list has no properties that can be dirty. + public IEnumerable GetDirtyProperties() => Enumerable.Empty(); - public void EnableChangeTracking() + public void ResetDirtyProperties() + { + foreach (IRememberBeingDirty dc in this.OfType()) { - // noop + dc.ResetDirtyProperties(); } + } - public void ResetWereDirtyProperties() - { - foreach (var dc in this.OfType()) - { - dc.ResetWereDirtyProperties(); - } - } + public void DisableChangeTracking() + { + // noop + } + + public void EnableChangeTracking() + { + // noop + } - public void ResetDirtyProperties(bool rememberDirty) + public void ResetWereDirtyProperties() + { + foreach (IRememberBeingDirty dc in this.OfType()) { - foreach (var dc in this.OfType()) - { - dc.ResetDirtyProperties(rememberDirty); - } + dc.ResetWereDirtyProperties(); } + } - /// Always return an empty enumerable, the list has no properties that can be dirty. - public IEnumerable GetWereDirtyProperties() + public void ResetDirtyProperties(bool rememberDirty) + { + foreach (IRememberBeingDirty dc in this.OfType()) { - return Enumerable.Empty(); + dc.ResetDirtyProperties(rememberDirty); } - - public event PropertyChangedEventHandler? PropertyChanged; // noop - #endregion } + + /// 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 f4702f01247d..579716456bfb 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 - { - public EventClearingObservableCollection() - { - } + // 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(List list) : base(list) - { - } + public EventClearingObservableCollection() + { + } - public EventClearingObservableCollection(IEnumerable collection) : base(collection) - { - } + public EventClearingObservableCollection(List list) + : base(list) + { + } - // 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; } - } + public EventClearingObservableCollection(IEnumerable collection) + : base(collection) + { + } - /// - /// Clears all event handlers for the event - /// - public void ClearCollectionChangedEvents() => _changed = null; + 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 148141f78331..4fc9edf3ae03 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 1ea6a827c4b7..9e52b4dae707 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; } - - /// - /// 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) + KeySelector = keySelector ?? throw new ArgumentException(nameof(keySelector)); + Indecies = new Dictionary(equalityComparer); + } + + 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); + return false; } - #region Protected Methods + RemoveAt(Indecies[key]); + return true; + } - 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)); + event NotifyCollectionChangedEventHandler? INotifyCollectionChanged.CollectionChanged + { + add => _changed += value; + remove => _changed -= value; + } - if (index != Count) + public bool ContainsKey(TKey key) => 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)) { - foreach (var k in Indecies.Keys.Where(k => Indecies[k] >= index).ToList()) - { - Indecies[k]++; - } + throw new InvalidOperationException("Key of new value does not match."); } - base.InsertItem(index, item); - Indecies[key] = index; + if (!Indecies.ContainsKey(key)) + { + Add(value); + } + else + { + this[Indecies[key]] = value; + } } + } + + /// + /// Clears all event handlers + /// + public void ClearCollectionChangedEvents() => _changed = null; - protected override void ClearItems() + /// + /// 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)) { - base.ClearItems(); - Indecies.Clear(); + return false; } - protected override void RemoveItem(int index) + // confirm key matches + if (!KeySelector(value)!.Equals(key)) { - 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]--; - } + throw new InvalidOperationException("Key of new value does not match."); } - #endregion + this[Indecies[key]] = value; + return true; + } - // 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 + public void ReplaceAll(IEnumerable values) + { + if (values == null) { - add { _changed += value; } - remove { _changed -= value; } + throw new ArgumentNullException(nameof(values)); } - /// - /// Clears all event handlers - /// - public void ClearCollectionChangedEvents() => _changed = null; + Clear(); - public bool ContainsKey(TKey key) + foreach (TValue value in values) { - return Indecies.ContainsKey(key); + Add(value); } + } - /// - /// 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] + /// + /// Allows us to change the key of an item + /// + /// + /// + public void ChangeKey(TKey currentKey, TKey newKey) + { + if (!Indecies.ContainsKey(currentKey)) { - - 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; - } - } + throw new InvalidOperationException($"No item with the key '{currentKey}' was found in the dictionary."); } - /// - /// 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 (ContainsKey(newKey)) { - if (!Indecies.ContainsKey(key)) return false; + throw new ArgumentException($"An element with the same key '{newKey}' already exists in the dictionary.", nameof(newKey)); + } - //confirm key matches - if (!KeySelector(value)!.Equals(key)) - throw new InvalidOperationException("Key of new value does not match."); + var currentIndex = Indecies[currentKey]; - this[Indecies[key]] = value; - return true; + Indecies.Remove(currentKey); + Indecies.Add(newKey, currentIndex); + } - } + #region Protected Methods - public void ReplaceAll(IEnumerable values) + protected override void InsertItem(int index, TValue item) + { + TKey key = KeySelector(item); + if (Indecies.ContainsKey(key)) { - if (values == null) throw new ArgumentNullException(nameof(values)); - - Clear(); + throw new ArgumentException($"An element with the same key '{key}' already exists in the dictionary.", nameof(item)); + } - foreach (var value in values) + if (index != Count) + { + foreach (TKey k in Indecies.Keys.Where(k => Indecies[k] >= index).ToList()) { - Add(value); + Indecies[k]++; } } - public bool Remove(TKey key) - { - if (!Indecies.ContainsKey(key)) return false; - - RemoveAt(Indecies[key]); - return true; + base.InsertItem(index, item); + Indecies[key] = index; + } - } + protected override void ClearItems() + { + base.ClearItems(); + Indecies.Clear(); + } - /// - /// 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."); - } + protected override void RemoveItem(int index) + { + TValue item = this[index]; + TKey key = KeySelector(item); - if (ContainsKey(newKey)) - { - throw new ArgumentException($"An element with the same key '{newKey}' already exists in the dictionary.", nameof(newKey)); - } + base.RemoveItem(index); - var currentIndex = Indecies[currentKey]; + Indecies.Remove(key); - Indecies.Remove(currentKey); - Indecies.Add(newKey, currentIndex); + foreach (TKey k in Indecies.Keys.Where(k => Indecies[k] > index).ToList()) + { + Indecies[k]--; } + } + + #endregion - #region IDictionary and IReadOnlyDictionary implementation + #region IDictionary and IReadOnlyDictionary implementation - public bool TryGetValue(TKey key, out TValue val) + public bool TryGetValue(TKey key, out TValue val) + { + if (Indecies.TryGetValue(key, out var index)) { - if (Indecies.TryGetValue(key, out var index)) - { - val = this[index]; - return true; - } - val = default!; - return false; + val = this[index]; + return true; } - /// - /// Returns all keys - /// - public IEnumerable Keys => Indecies.Keys; + val = default!; + return false; + } - /// - /// Returns all values - /// - public IEnumerable Values => base.Items; + /// + /// Returns all keys + /// + public IEnumerable Keys => Indecies.Keys; - ICollection IDictionary.Keys => Indecies.Keys; + /// + /// Returns all values + /// + public IEnumerable Values => Items; - //this will never be used - ICollection IDictionary.Values => Values.ToList(); + ICollection IDictionary.Keys => Indecies.Keys; - bool ICollection>.IsReadOnly => false; + // this will never be used + ICollection IDictionary.Values => Values.ToList(); - IEnumerator> IEnumerable>.GetEnumerator() - { - foreach (var i in Values) - { - var key = KeySelector(i); - yield return new KeyValuePair(key, i); - } - } + bool ICollection>.IsReadOnly => false; - void IDictionary.Add(TKey key, TValue value) + IEnumerator> IEnumerable>.GetEnumerator() + { + foreach (TValue i in Values) { - Add(value); + TKey key = KeySelector(i); + yield return new KeyValuePair(key, i); } + } - void ICollection>.Add(KeyValuePair item) - { - Add(item.Value); - } + void IDictionary.Add(TKey key, TValue value) => Add(value); - bool ICollection>.Contains(KeyValuePair item) - { - return ContainsKey(item.Key); - } + void ICollection>.Add(KeyValuePair item) => Add(item.Value); - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - { - throw new NotImplementedException(); - } + bool ICollection>.Contains(KeyValuePair item) => ContainsKey(item.Key); - bool ICollection>.Remove(KeyValuePair item) - { - return Remove(item.Key); - } + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => + throw new NotImplementedException(); - #endregion - } + 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 e5a34083be8a..d23c81a7b27d 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; + private readonly bool _keepOldest; - public OrderedHashSet(bool keepOldest = true) + public OrderedHashSet(bool keepOldest = true) => _keepOldest = keepOldest; + + protected override void InsertItem(int index, T item) + { + 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 we want to keep the newest, then we need to remove the old item and add the new one - if (exists == false) + if (Remove(item)) { - base.InsertItem(index, item); + index--; } - else if(_keepOldest == false) - { - if (Remove(item)) - { - index--; - } - base.InsertItem(index, item); - } - } - } - protected override T GetKeyForItem(T item) - { - return item; + base.InsertItem(index, 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 242766771d3b..2324eec892ec 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 (); + private readonly LinkedList _linkedList = new(); - public int Count => _linkedList.Count; + public int Count => _linkedList.Count; - public void Clear() => _linkedList.Clear(); + public void Clear() => _linkedList.Clear(); - public void Push(T? obj) => _linkedList.AddFirst(obj); + public void Push(T? obj) => _linkedList.AddFirst(obj); - public void Enqueue(T? obj) => _linkedList.AddFirst(obj); + public void Enqueue(T? obj) => _linkedList.AddFirst(obj); - public T Pop() + 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() + _linkedList.RemoveFirst(); + return obj!; + } + + public T Dequeue() + { + var obj = default(T); + if (_linkedList.Last is not null) { - T? obj = default(T); - if (_linkedList.Last is not null) - { - obj = _linkedList.Last.Value; - } - _linkedList.RemoveLast(); - return obj!; + obj = _linkedList.Last.Value; } - 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.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 11fd155684eb..fd2161c6d3a4 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."; + internal const string CycleDependencyError = "Cyclic dependency."; + internal const string MissingDependencyError = "Missing dependency."; - public class Node - { - public Node(TKey key, TItem item, IEnumerable dependencies) - { - Key = key; - Item = item; - Dependencies = dependencies; - } + public static Node CreateNode(TKey key, TItem item, IEnumerable dependencies) => + new(key, item, dependencies); - public TKey Key { get; } - public TItem Item { get; } - public IEnumerable Dependencies { get; } + public class Node + { + public Node(TKey key, TItem item, IEnumerable dependencies) + { + 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(); /// - /// Represents a generic DAG that can be topologically sorted. + /// Initializes a new instance of the class. /// - /// The type of the keys. - /// The type of the items. - public class TopoGraph : TopoGraph - where TKey : notnull + /// 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) { - private readonly Func _getKey; - private readonly Func?> _getDependencies; - private readonly Dictionary _items = new Dictionary(); - - /// - /// 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; - } + _getKey = getKey; + _getDependencies = getDependencies; + } - /// - /// Adds an item to the graph. - /// - /// The item. - public void AddItem(TItem item) - { - var key = _getKey(item); - _items[key] = item; - } + /// + /// Adds an item to the graph. + /// + /// The item. + public void AddItem(TItem item) + { + TKey key = _getKey(item); + _items[key] = item; + } - /// - /// Adds items to the graph. - /// - /// The items. - public void AddItems(IEnumerable items) + /// + /// Adds items to the graph. + /// + /// The items. + public void AddItems(IEnumerable items) + { + foreach (TItem item in items) { - foreach (var item in items) - AddItem(item); + 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; - } + /// + /// 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; - private static bool Contains(TItem[] items, TItem item, int start, int count) + foreach (TItem item in _items.Values) { - return Array.IndexOf(items, item, start, count) >= 0; + Visit(item, visited, sorted, ref index, incr, throwOnCycle, throwOnMissing); } - private void Visit(TItem item, ISet visited, TItem[] sorted, ref int index, int incr, bool throwOnCycle, bool throwOnMissing) + 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)) { - 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); + return; + } - var keys = _getDependencies(item); - var dependencies = keys == null ? null : FindDependencies(keys, throwOnMissing); + visited.Add(item); - if (dependencies != null) - foreach (var dep in dependencies) - Visit(dep, visited, sorted, ref index, incr, throwOnCycle, throwOnMissing); + IEnumerable? keys = _getDependencies(item); + IEnumerable? dependencies = keys == null ? null : FindDependencies(keys, throwOnMissing); - sorted[index] = item; - index += incr; + if (dependencies != null) + { + foreach (TItem dep in dependencies) + { + Visit(dep, visited, sorted, ref index, incr, throwOnCycle, throwOnMissing); + } } - private IEnumerable FindDependencies(IEnumerable keys, bool throwOnMissing) + sorted[index] = item; + index += incr; + } + + private IEnumerable FindDependencies(IEnumerable keys, bool throwOnMissing) + { + foreach (TKey key in keys) { - foreach (var key in keys) + if (_items.TryGetValue(key, out TItem? value)) + { + yield return value; + } + else if (throwOnMissing) { - 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}"); + 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 96565a843c9f..ab51dd56b2f0 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(); - - /// - /// Adds a type to the list. - /// - /// The type to add. - public void Add() - where T : TBase - { - _list.Add(typeof(T)); - } + /// 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 1af9511fb77a..ffacd89cffe0 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; - /// - /// Provides a base class for builder collections. + /// Initializes a new instance of the + /// + /// with items. /// - /// The type of the items. - public abstract class BuilderCollectionBase : IBuilderCollection - { - private readonly LazyReadOnlyCollection _items; + /// The items. + public BuilderCollectionBase(Func> items) => _items = new 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; - /// - public int Count => _items.Count; - - /// - /// Gets an enumerator. - /// - public IEnumerator GetEnumerator() => ((IEnumerable)_items).GetEnumerator(); + /// + /// Gets an enumerator. + /// + public IEnumerator GetEnumerator() => _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 8b5913ab1d32..8b1c33a6100f 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) - { - if (_registeredTypes != null) - throw new InvalidOperationException("This builder has already been registered."); + // register the types + RegisterTypes(services); + } - // register the collection - services.Add(new ServiceDescriptor(typeof(TCollection), CreateCollection, CollectionLifetime)); + /// + /// Creates a collection. + /// + /// A collection. + /// Creates a new collection each time it is invoked. + public virtual TCollection CreateCollection(IServiceProvider factory) + => factory.CreateInstance(CreateItemsFactory(factory)); - // register the types - RegisterTypes(services); - } + /// + /// 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 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) + /// + /// 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) { - lock (_locker) + if (_registeredTypes != null) { - 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; + action(_types); } + } - private void RegisterTypes(IServiceCollection services) - { - lock (_locker) - { - if (_registeredTypes != null) - return; - - var types = GetRegisteringTypes(_types).ToArray(); + /// + /// 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; - // ensure they are safe - foreach (var type in types) - EnsureType(type, "register"); + /// + /// 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."); + } - // 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)); + return _registeredTypes // respect order + .Select(x => CreateItem(factory, x)) + .ToArray(); // safe + } - _registeredTypes = types; - } - } + /// + /// Creates a collection item. + /// + protected virtual TItem CreateItem(IServiceProvider factory, Type itemType) + => (TItem)factory.GetRequiredService(itemType); - /// - /// Creates the collection items. - /// - /// The collection items. - protected virtual IEnumerable CreateItems(IServiceProvider factory) + protected Type EnsureType(Type type, string action) + { + if (typeof(TItem).IsAssignableFrom(type) == false) { - 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 + throw new InvalidOperationException( + $"Cannot {action} type {type.FullName} as it does not inherit from/implement {typeof(TItem).FullName}."); } - /// - /// Creates a collection item. - /// - protected virtual TItem CreateItem(IServiceProvider factory, Type itemType) - => (TItem)factory.GetRequiredService(itemType); + return type; + } - /// - /// Creates a collection. - /// - /// A collection. - /// Creates a new collection each time it is invoked. - public virtual TCollection CreateCollection(IServiceProvider factory) - => factory.CreateInstance(CreateItemsFactory(factory)); + private void RegisterTypes(IServiceCollection services) + { + lock (_locker) + { + if (_registeredTypes != null) + { + return; + } - // used to resolve a Func> parameter - private Func> CreateItemsFactory(IServiceProvider factory) => () => CreateItems(factory); + Type[] types = GetRegisteringTypes(_types).ToArray(); - 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; - } + // ensure they are safe + foreach (Type type in types) + { + EnsureType(type, "register"); + } - /// - /// 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)); - } + // 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)); + } - /// - /// 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); + _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 c39dd503e0bc..d64de626d026 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 const int LogThresholdMilliseconds = 100; + private readonly ILogger _logger; - private readonly IProfilingLogger _profilingLogger; - private readonly ILogger _logger; + private readonly IProfilingLogger _profilingLogger; - public ComponentCollection(Func> items, IProfilingLogger profilingLogger, ILogger logger) - : base(items) - { - _profilingLogger = profilingLogger; - _logger = logger; - } + public ComponentCollection(Func> items, IProfilingLogger profilingLogger, ILogger logger) + : base(items) + { + _profilingLogger = profilingLogger; + _logger = logger; + } - public void Initialize() + public void Initialize() + { + using (_profilingLogger.DebugDuration( + $"Initializing. (log components when >{LogThresholdMilliseconds}ms)", "Initialized.")) { - 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 + { + component.Terminate(); + component.DisposeIfDisposable(); + } + catch (Exception ex) { - try - { - component.Terminate(); - component.DisposeIfDisposable(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error while terminating component {ComponentType}.", componentType.FullName); - } + _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 1e36c4e8e988..b77dfde819a1 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; + private const int LogThresholdMilliseconds = 100; - public ComponentCollectionBuilder() - { } + protected override ComponentCollectionBuilder This => this; - protected override ComponentCollectionBuilder This => this; + protected override IEnumerable CreateItems(IServiceProvider factory) + { + IProfilingLogger logger = factory.GetRequiredService(); - 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) - { - var logger = factory.GetRequiredService(); + protected override IComponent CreateItem(IServiceProvider factory, Type itemType) + { + IProfilingLogger logger = factory.GetRequiredService(); - using (logger.DebugDuration($"Creating {itemType.FullName}.", $"Created {itemType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) - { - return base.CreateItem(factory, itemType); - } + using (logger.DebugDuration( + $"Creating {itemType.FullName}.", + $"Created {itemType.FullName}.", + thresholdMilliseconds: LogThresholdMilliseconds)) + { + return base.CreateItem(factory, itemType); } } } diff --git a/src/Umbraco.Core/Composing/ComponentComposer.cs b/src/Umbraco.Core/Composing/ComponentComposer.cs index c1d921df03ee..2a9641e64b5a 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 c12ddbcd3e98..bd3567595d83 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. /// - /// - /// 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 + /// The type of the required composer. + public ComposeAfterAttribute(Type requiredType) { - /// - /// Initializes a new instance of the class. - /// - /// The type of the required composer. - public ComposeAfterAttribute(Type requiredType) + if (typeof(IComposer).IsAssignableFrom(requiredType) == false) { - 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; + throw new ArgumentException( + $"Type {requiredType.FullName} is invalid here because it does not implement {typeof(IComposer).FullName}."); } - /// - /// 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; - } + RequiredType = requiredType; + } - /// - /// Gets the required type. - /// - public Type RequiredType { get; } + /// + /// 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 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; } - } + /// + /// 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; } } diff --git a/src/Umbraco.Core/Composing/ComposeBeforeAttribute.cs b/src/Umbraco.Core/Composing/ComposeBeforeAttribute.cs index 382772de8d58..c41f1e50742e 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 510d59b3749c..3c602b0ad93c 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; + + /// + /// 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) + { + _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)); + } /// - /// Handles the composers. + /// Instantiates and composes the composers. /// - internal class ComposerGraph + public void Compose() { - private readonly IUmbracoBuilder _builder; - private readonly ILogger _logger; - private readonly IEnumerable _composerTypes; - private readonly IEnumerable _enableDisableAttributes; - - /// - /// 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) + // 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); } + } - private class EnableInfo + 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) { - public bool Enabled { get; set; } - public int Weight { get; set; } = -1; + return types.Any(x => type.IsAssignableFrom(x) && !x.IsInterface); } - /// - /// Instantiates and composes the composers. - /// - public void Compose() + foreach (KeyValuePair?> kvp in requirements) { - // make sure it is there - _builder.WithCollectionBuilder(); + Type type = kvp.Key; - IEnumerable orderedComposerTypes = PrepareComposerTypes(); + text.AppendLine(type.FullName); + foreach (ComposeAfterAttribute 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 (IComposer composer in InstantiateComposers(orderedComposerTypes)) + foreach (ComposeBeforeAttribute attribute in type.GetCustomAttributes()) { - composer.Compose(_builder); + text.AppendLine(" < " + attribute.RequiringType); } + + foreach (Type i in type.GetInterfaces()) + { + text.AppendLine(" : " + i.FullName); + } + + if (kvp.Value != null) + { + foreach (Type t in kvp.Value) + { + text.AppendLine(" = " + t); + } + } + + text.AppendLine(); } - internal IEnumerable PrepareComposerTypes() - { - var requirements = GetRequirements(); + text.AppendLine("/"); + text.AppendLine(); + return text.ToString(); + } - // only for debugging, this is verbose - //_logger.Debug(GetComposersReport(requirements)); + internal IEnumerable PrepareComposerTypes() + { + Dictionary?> requirements = GetRequirements(); - var sortedComposerTypes = SortComposers(requirements); + // 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); + // 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; - } + 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(); + 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); + // enable or disable composers + EnableDisableComposers(_enableDisableAttributes, composerTypeList); - void GatherInterfaces(Type type, Func getTypeInAttribute, HashSet iset, List set2) - where TAttribute : Attribute + static void GatherInterfaces(Type type, Func getTypeInAttribute, HashSet iset, List set2) + where TAttribute : Attribute + { + foreach (TAttribute attribute in type.GetCustomAttributes()) { - foreach (var attribute in type.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 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); + + // add all its interfaces implementing IComposer + foreach (Type i in typeInAttribute.GetInterfaces() + .Where(x => typeof(IComposer).IsAssignableFrom(x))) { - // add it to the new list - iset.Add(typeInAttribute); - set2.Add(typeInAttribute); - - // add all its interfaces implementing IComposer - foreach (var i in typeInAttribute.GetInterfaces().Where(x => typeof(IComposer).IsAssignableFrom(x))) - { - iset.Add(i); - set2.Add(i); - } + iset.Add(i); + set2.Add(i); } } } + } - // 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) + // 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) + { + var list2 = new List(); + foreach (Type t in list1) { - 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; + GatherInterfaces(t, a => a.RequiredType, interfaces, list2); + GatherInterfaces(t, a => a.RequiringType, interfaces, 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); - } + composerTypeList.AddRange(list2); + list1 = list2; + } + + // sort the composers according to their dependencies + var requirements = new Dictionary?>(); + foreach (Type type in composerTypeList) + { + requirements[type] = null; + } - return requirements; + foreach (Type type in composerTypeList) + { + GatherRequirementsFromAfterAttribute(type, composerTypeList, requirements, throwOnMissing); + GatherRequirementsFromBeforeAttribute(type, composerTypeList, requirements); } - internal IEnumerable SortComposers(Dictionary?> 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) { - // sort composers - var graph = new TopoGraph?>>(kvp => kvp.Key, kvp => kvp.Value); - graph.AddItems(requirements); - List sortedComposerTypes; - try + if (enabled.TryGetValue(composerType, out EnableInfo? enableInfo) == false) { - sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).Where(x => !x.IsInterface).ToList(); + enableInfo = enabled2[composerType] = new EnableInfo(); } - catch (Exception e) + + if (enableInfo.Weight > weight2) { - // 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; } - return sortedComposerTypes; + enableInfo.Enabled = value; + enableInfo.Weight = weight2; } - internal static string GetComposersReport(Dictionary?> requirements) + foreach (EnableComposerAttribute attr in enableDisableAttributes.OfType()) { - 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; + Type type = attr.EnabledType; + UpdateEnableInfo(type, 2, enabled, true); + } - 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(); + foreach (DisableComposerAttribute attr in enableDisableAttributes.OfType()) + { + Type type = attr.DisabledType; + UpdateEnableInfo(type, 2, enabled, false); } - private static void EnableDisableComposers(IEnumerable enableDisableAttributes, ICollection types) + foreach (Type composerType in 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) + foreach (EnableAttribute attr in composerType.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; + Type type = attr.EnabledType ?? composerType; + var weight = type == composerType ? 1 : 3; + UpdateEnableInfo(type, weight, enabled, true); } - foreach (var attr in enableDisableAttributes.OfType()) + foreach (DisableAttribute attr in composerType.GetCustomAttributes()) { - var type = attr.EnabledType; - UpdateEnableInfo(type, 2, enabled, true); + Type type = attr.DisabledType ?? composerType; + var weight = type == composerType ? 1 : 3; + UpdateEnableInfo(type, weight, enabled, false); } + } - foreach (var attr in enableDisableAttributes.OfType()) + // 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) { - var type = attr.DisabledType; - UpdateEnableInfo(type, 2, enabled, false); + continue; // ignore self-requirements (+ exclude in implems, below) } - foreach (var composerType in types) + // 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) { - foreach (var attr in composerType.GetCustomAttributes()) + var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x) && !x.IsInterface) + .ToList(); + if (implems.Count > 0) { - var type = attr.EnabledType ?? composerType; - var weight = type == composerType ? 1 : 3; - UpdateEnableInfo(type, weight, enabled, true); + if (requirements[type] == null) + { + requirements[type] = new List(); + } + + requirements[type]!.AddRange(implems); } - foreach (var attr in composerType.GetCustomAttributes()) + // if explicitly set to !weak, is strong, else is weak + else if (attr.Weak == false && throwOnMissing) { - var type = attr.DisabledType ?? composerType; - var weight = type == composerType ? 1 : 3; - UpdateEnableInfo(type, weight, enabled, false); + throw new Exception( + $"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); } } - // 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) + // requiring a class = require that the composer is enabled + // unless weak, and then requires it if it is enabled + else { - 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) + if (types.Contains(attr.RequiredType)) { - var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x) && !x.IsInterface).ToList(); - if (implems.Count > 0) + if (requirements[type] == null) { - if (requirements[type] == null) requirements[type] = new List(); - requirements[type]!.AddRange(implems); + requirements[type] = new List(); } - 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}."); + + requirements[type]!.Add(attr.RequiredType); } - // requiring a class = require that the composer is enabled - // unless weak, and then requires it if it is enabled - else + + // if not explicitly set to weak, is strong + else if (attr.Weak != true && throwOnMissing) { - 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}."); + throw new Exception( + $"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); } } } + } - private static void GatherRequirementsFromBeforeAttribute(Type type, ICollection types, IDictionary?> requirements) + 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) { - // 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) { - if (attr.RequiringType == type) continue; // ignore self-requirements (+ exclude in implems, below) + continue; // ignore self-requirements (+ exclude in implems, below) + } - // required by an interface = by any enabled composer implementing this that interface - if (attr.RequiringType.IsInterface) + // 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) { - var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x) && !x.IsInterface).ToList(); - foreach (var implem in implems) + if (requirements[implem] == null) { - if (requirements[implem] == null) requirements[implem] = new List(); - requirements[implem]!.Add(type); + requirements[implem] = new List(); } + + requirements[implem]!.Add(type); } - // required by a class - else + } + + // required by a class + else + { + if (types.Contains(attr.RequiringType)) { - if (types.Contains(attr.RequiringType)) + if (requirements[attr.RequiringType] == null) { - if (requirements[attr.RequiringType] == null) requirements[attr.RequiringType] = new List(); - requirements[attr.RequiringType]!.Add(type); + requirements[attr.RequiringType] = new List(); } + + requirements[attr.RequiringType]!.Add(type); } } } + } - private static IEnumerable InstantiateComposers(IEnumerable types) + private static IEnumerable InstantiateComposers(IEnumerable types) + { + foreach (Type type in 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."); - } + ConstructorInfo? ctor = type.GetConstructor(Array.Empty()); - yield return (IComposer) ctor.Invoke(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 d087af77d845..2906070e4f1e 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 5cc38f31a7b1..0f1d0cc571b0 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 -{ - /// - /// 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 Assembly _entryPointAssembly; - private readonly ILoggerFactory _loggerFactory; - private readonly IEnumerable? _additionalTargetAssemblies; - private List? _discovered; +namespace Umbraco.Cms.Core.Composing; - public DefaultUmbracoAssemblyProvider( - Assembly? entryPointAssembly, - ILoggerFactory loggerFactory, - IEnumerable? additionalTargetAssemblies = null) - { - _entryPointAssembly = entryPointAssembly ?? throw new ArgumentNullException(nameof(entryPointAssembly)); - _loggerFactory = loggerFactory; - _additionalTargetAssemblies = additionalTargetAssemblies; - } +/// +/// 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; - // 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 DefaultUmbracoAssemblyProvider( + Assembly? entryPointAssembly, + ILoggerFactory loggerFactory, + IEnumerable? additionalTargetAssemblies = null) + { + _entryPointAssembly = entryPointAssembly ?? throw new ArgumentNullException(nameof(entryPointAssembly)); + _loggerFactory = loggerFactory; + _additionalTargetAssemblies = additionalTargetAssemblies; + } - public IEnumerable Assemblies + // 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 { - get + if (_discovered != null) { - if (_discovered != null) - { - return _discovered; - } + return _discovered; + } - IEnumerable additionalTargetAssemblies = Constants.Composing.UmbracoCoreAssemblyNames; - if (_additionalTargetAssemblies != null) - { - additionalTargetAssemblies = additionalTargetAssemblies.Concat(_additionalTargetAssemblies); - } + 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(); + var finder = new FindAssembliesWithReferencesTo( + new[] { _entryPointAssembly }, + additionalTargetAssemblies.ToArray(), + true, + _loggerFactory); + _discovered = finder.Find().ToList(); - return _discovered; - } + return _discovered; } } } diff --git a/src/Umbraco.Core/Composing/DisableAttribute.cs b/src/Umbraco.Core/Composing/DisableAttribute.cs index 23d825ee1c98..09d638188da2 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); - } + 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; - } + /// + /// 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; } - } + /// + /// 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 59b36178cfdc..2c85d45b46e1 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 90fb1a9cc6e9..7ca33d50b7da 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; - } + /// + /// 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; } - } + /// + /// 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 048a19a80f49..b1a0f53bcd11 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 78cdb80f58fc..f9e4ed6dbeef 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 +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 bool _includeTargets; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly Assembly[] _referenceAssemblies; + private readonly string[] _targetAssemblies; + /// - /// Finds Assemblies from the entry point assemblies, it's dependencies and it's transitive dependencies that reference that targetAssemblyNames + /// Constructor /// - /// - /// 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 + /// 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) { - private readonly Assembly[] _referenceAssemblies; - private readonly string[] _targetAssemblies; - private readonly bool _includeTargets; - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; + _referenceAssemblies = referenceAssemblies; + _targetAssemblies = targetAssemblyNames; + _includeTargets = includeTargets; + _loggerFactory = loggerFactory; + _logger = _loggerFactory.CreateLogger(); + } - /// - /// 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) + 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) - { - referenceItems.Add(assembly); - } - - if (_includeTargets) + foreach (var target in _targetAssemblies) { - 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 b985a7949417..4478deb5ac85 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 fdc942ae2438..4148c9ee4788 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 5e78cf0c2f69..56036997bc98 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 ea09558cad87..da25a548e72f 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); - } + 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 +{ /// - /// Represents a collection builder. + /// Creates a collection. /// - /// 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); - } + /// 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 8e9cf815e87c..d5655f8a1f81 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 6f1978ee3e10..7d0a85931434 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 153fde36b632..848c70ddab7c 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 b19b22a7e914..d641c9053803 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 7d59b688693f..4bebfae33467 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 fe5af3a98544..a3e45054f820 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 baae385af489..49ada40dfa42 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; - } + 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 + /// + /// Adds a type to the collection. + /// + /// The type to add. + /// The builder. + public TBuilder Add() + where T : TItem + { + Configure(types => { - 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) { - EnsureType(type, "register"); - if (types.Contains(type) == false) - types.Add(type); - }); - return This; - } + types.Add(type); + } + }); + return This; + } - /// - /// Adds a types producer to the collection. - /// - /// The types producer. - /// The builder. - public TBuilder Add(Func> producer) + /// + /// Adds a types producer to the collection. + /// + /// The types producer. + /// The builder. + public TBuilder Add(Func> producer) + { + Configure(types => { - Configure(types => - { - _producers.Add(producer); - }); - return This; - } + _producers.Add(producer); + }); + return This; + } - /// - /// Excludes a type from the collection. - /// - /// The type to exclude. - /// The builder. - public TBuilder Exclude() - where T : TItem + /// + /// Excludes a type from the collection. + /// + /// The type to exclude. + /// The builder. + public TBuilder Exclude() + where T : TItem + { + Configure(types => { - Configure(types => + Type type = typeof(T); + if (_excluded.Contains(type) == false) { - var type = typeof(T); - if (_excluded.Contains(type) == false) - _excluded.Add(type); - }); - return This; - } + _excluded.Add(type); + } + }); + return This; + } - /// - /// Excludes a type from the collection. - /// - /// The type to exclude. - /// The builder. - public TBuilder Exclude(Type type) + /// + /// Excludes a type from the collection. + /// + /// The type to exclude. + /// The builder. + public TBuilder Exclude(Type type) + { + Configure(types => { - Configure(types => + EnsureType(type, "exclude"); + if (_excluded.Contains(type) == false) { - 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); - } + _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 67116524acb9..eb6f2ed055af 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; + private readonly Lazy> _lazyCollection; + private int? _count; - public LazyReadOnlyCollection(Lazy> lazyCollection) => _lazyCollection = lazyCollection; + public LazyReadOnlyCollection(Lazy> lazyCollection) => _lazyCollection = lazyCollection; - public LazyReadOnlyCollection(Func> lazyCollection) => _lazyCollection = new Lazy>(lazyCollection); + public LazyReadOnlyCollection(Func> lazyCollection) => + _lazyCollection = new Lazy>(lazyCollection); - public IEnumerable Value => EnsureCollection(); + public IEnumerable Value => EnsureCollection(); - private IEnumerable EnsureCollection() + public int Count + { + get { - if (_lazyCollection == null) - { - _count = 0; - return Enumerable.Empty(); - } - - IEnumerable val = _lazyCollection.Value; - if (_count == null) - { - _count = val.Count(); - } - return val; + EnsureCollection(); + return _count.GetValueOrDefault(); } + } + + public IEnumerator GetEnumerator() => Value.GetEnumerator(); - public int Count + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private IEnumerable EnsureCollection() + { + if (_lazyCollection == null) { - get - { - EnsureCollection(); - return _count.GetValueOrDefault(); - } + _count = 0; + return Enumerable.Empty(); } - public IEnumerator GetEnumerator() => Value.GetEnumerator(); + IEnumerable val = _lazyCollection.Value; + if (_count == null) + { + _count = val.Count(); + } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + return val; } } diff --git a/src/Umbraco.Core/Composing/LazyResolve.cs b/src/Umbraco.Core/Composing/LazyResolve.cs index afa22f74b661..723d9afe2e35 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 939561f557b7..d9c733da7d18 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() - { - Configure(types => types.Clear()); - return This; - } - - /// - /// Appends a type to the collection. - /// - /// The type to append. - /// The builder. - public TBuilder Append() - where T : TItem + /// + /// Appends a type to the collection. + /// + /// The type to append. + /// The builder. + public TBuilder Append() + 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); - types.Add(type); - }); - return This; - } - - /// - /// Appends a type to the collection. - /// - /// The type to append. - /// The builder. - public TBuilder Append(Type type) + types.Remove(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)) { - 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) + 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 => { - Configure(list => + foreach (Type type in types) { - foreach (var type in types) + // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast + EnsureType(type, "register"); + 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)) { - var 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) + 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 => { - return 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) + 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 => { - Configure(types => + Type typeBefore = typeof(TBefore); + Type type = typeof(T); + if (typeBefore == type) { - 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 + 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 => { - Configure(types => + EnsureType(typeBefore, "find"); + EnsureType(type, "register"); + + if (typeBefore == type) + { + throw new InvalidOperationException(); + } + + var index = types.IndexOf(typeBefore); + if (index < 0) { - var typeBefore = typeof(TBefore); - var type = typeof(T); - if (typeBefore == type) throw new InvalidOperationException(); + throw new InvalidOperationException(); + } - var index = types.IndexOf(typeBefore); - if (index < 0) throw new InvalidOperationException(); + if (types.Contains(type)) + { + types.Remove(type); + } - 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) + 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 => { - Configure(types => + Type typeAfter = typeof(TAfter); + Type type = typeof(T); + if (typeAfter == type) { - EnsureType(typeBefore, "find"); - EnsureType(type, "register"); + throw new InvalidOperationException(); + } + + var index = types.IndexOf(typeAfter); + if (index < 0) + { + throw new InvalidOperationException(); + } - if (typeBefore == type) throw new InvalidOperationException(); + if (types.Contains(type)) + { + types.Remove(type); + } - var index = types.IndexOf(typeBefore); - if (index < 0) throw new InvalidOperationException(); + index = types.IndexOf(typeAfter); // in case removing type changed index + index += 1; // insert here - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeBefore); // in case removing type changed index + if (index == types.Count) + { + types.Add(type); + } + else + { 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 + } + }); + 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) + { + Configure(types => { - Configure(types => + EnsureType(typeAfter, "find"); + EnsureType(type, "register"); + + if (typeAfter == type) { - var typeAfter = typeof(TAfter); - var 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 - types.Insert(index, type); - }); - 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) - { - Configure(types => + throw new InvalidOperationException(); + } + + var index = types.IndexOf(typeAfter); + if (index < 0) { - EnsureType(typeAfter, "find"); - EnsureType(type, "register"); + 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; + } - 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 + /// + /// 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; - } - - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove(Type type) + types.Remove(type); + } + }); + return This; + } + + /// + /// 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; - } - - /// - /// 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 + 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 + { + 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; - } - - /// - /// 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 => + if (types.Contains(type)) { - EnsureType(typeReplaced, "find"); - EnsureType(type, "register"); + types.Remove(type); + } + + index = types.IndexOf(typeReplaced); // in case removing type changed index + types.Insert(index, type); + types.Remove(typeReplaced); + }); + return This; + } - if (typeReplaced == type) return; + /// + /// 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"); - var index = types.IndexOf(typeReplaced); - if (index < 0) throw new InvalidOperationException(); + if (typeReplaced == type) + { + return; + } - 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; - } + 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; } } diff --git a/src/Umbraco.Core/Composing/ReferenceResolver.cs b/src/Umbraco.Core/Composing/ReferenceResolver.cs index 5b7c5ffde9a5..1924fb4b75be 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 { + 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) + { + _umbracoAssemblies = new HashSet(targetAssemblies, StringComparer.Ordinal); + _assemblies = entryPointAssemblies; + _logger = logger; + _classifications = new Dictionary(); + + foreach (Assembly item in entryPointAssemblies) + { + _lookup.Add(item); + } + } + + protected enum Classification + { + Unknown, + DoesNotReferenceUmbraco, + ReferencesUmbraco, + IsUmbraco, + } + /// - /// Resolves assemblies that reference one of the specified "targetAssemblies" either directly or transitively. + /// Returns a list of assemblies that directly reference or transitively reference the targetAssemblies /// + /// /// - /// Borrowed and modified from https://github.com/dotnet/aspnetcore-tooling/blob/master/src/Razor/src/Microsoft.NET.Sdk.Razor/ReferenceResolver.cs + /// This includes all assemblies in the same location as the entry point assemblies /// - internal class ReferenceResolver + public IEnumerable ResolveAssemblies() { - 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(); - - foreach (var 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() - { - var applicationParts = new List(); + var applicationParts = new List(); - var assemblies = new HashSet(_assemblies); + var assemblies = new HashSet(_assemblies); - // Get the unique directories of the assemblies - var assemblyLocations = GetAssemblyFolders(assemblies).ToList(); + // 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) + // 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) + 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 => + assemblyName.FullName.StartsWith(f, StringComparison.InvariantCultureIgnoreCase))) { - _logger.LogInformation(e, "Error: could not load {dll} for type scanning", dll); + continue; } - if (assemblyName != null) + // 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))) { - // 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); + continue; } - } - } - foreach (var item in assemblies) - { - var classification = Resolve(item); - if (classification == Classification.ReferencesUmbraco || classification == Classification.IsUmbraco) - { - applicationParts.Add(item); + var assembly = Assembly.Load(assemblyName); + assemblies.Add(assembly); } } - - 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) + foreach (Assembly item in assemblies) { - if (Uri.TryCreate(assembly.CodeBase, UriKind.Absolute, out var result) && - result.IsFile && string.IsNullOrWhiteSpace(result.Fragment)) + Classification classification = Resolve(item); + if (classification == Classification.ReferencesUmbraco || classification == Classification.IsUmbraco) { - return result.LocalPath; + applicationParts.Add(item); } - - return assembly.Location; } - private Classification Resolve(Assembly assembly) + return applicationParts; + } + + protected virtual IEnumerable GetReferences(Assembly assembly) + { + foreach (AssemblyName referenceName in assembly.GetReferencedAssemblies()) { - if (_classifications.TryGetValue(assembly, out var classification)) + // don't include if this is excluded + if (TypeFinder.KnownAssemblyExclusionFilter.Any(f => + referenceName.FullName.StartsWith(f, StringComparison.InvariantCultureIgnoreCase))) { - return classification; + continue; } - // Initialize the dictionary with a value to short-circuit recursive references. - classification = Classification.Unknown; - _classifications[assembly] = classification; + var reference = Assembly.Load(referenceName); - 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!)) + if (!_lookup.Contains(reference)) { - classification = Classification.IsUmbraco; + // 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); } - 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; - } - } - } + 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; + } - Debug.Assert(classification != Classification.Unknown); - _classifications[assembly] = classification; + 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)) + { + // 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 { - foreach (var referenceName in assembly.GetReferencedAssemblies()) + 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 5e0523f09dad..e66bedf79fff 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; + return _calculated; } + IEnumerable<(FileSystemInfo, bool)> allPaths = _paths.GetFolders() + .Select(x => ((FileSystemInfo)x, false)) + .Concat(_paths.GetFiles().Select(x => ((FileSystemInfo)x.Key, x.Value))); - 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); - _calculated = GetFileHash(allPaths); - - return _calculated; - } + 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) + /// + /// 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(); + // get the distinct file infos to hash + var uniqInfos = new HashSet(); + var uniqContent = new HashSet(); - using var generator = new HashGenerator(); + using var generator = new HashGenerator(); - foreach ((FileSystemInfo fileOrFolder, bool scanFileContent) in filesAndFolders) + foreach ((FileSystemInfo fileOrFolder, var 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; - } + continue; + } - using (FileStream fileStream = File.OpenRead(fileOrFolder.FullName)) - { - var hash = fileStream.GetStreamHash(); - generator.AddCaseInsensitiveString(hash); - } + using (FileStream fileStream = File.OpenRead(fileOrFolder.FullName)) + { + var hash = fileStream.GetStreamHash(); + generator.AddCaseInsensitiveString(hash); } } - else + } + else + { + // add each unique folder/file to the hash + if (uniqInfos.Add(fileOrFolder.FullName)) { - // add each unique folder/file to the hash - if (uniqInfos.Add(fileOrFolder.FullName)) - { - generator.AddFileSystemItem(fileOrFolder); - } + generator.AddFileSystemItem(fileOrFolder); } } - return generator.GenerateHash(); } - } + return generator.GenerateHash(); + } } } diff --git a/src/Umbraco.Core/Composing/RuntimeHashPaths.cs b/src/Umbraco.Core/Composing/RuntimeHashPaths.cs index eac2f83bcd1b..5720fdebe2e6 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 { + private readonly Dictionary _files = new(); + private readonly List _paths = new(); + + public RuntimeHashPaths AddFolder(DirectoryInfo pathInfo) + { + _paths.Add(pathInfo); + return this; + } + /// - /// Paths used to determine the + /// Creates a runtime hash based on the assembly provider /// - public sealed class RuntimeHashPaths + /// + /// + public RuntimeHashPaths AddAssemblies(IAssemblyProvider assemblyProvider) { - private readonly List _paths = new List(); - private readonly Dictionary _files = new Dictionary(); - - public RuntimeHashPaths AddFolder(DirectoryInfo pathInfo) + foreach (Assembly assembly in assemblyProvider.Assemblies) { - _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) { - // TODO: We need to test this on a published website - if (!assembly.IsDynamic && assembly.Location != null) - { - AddFile(new FileInfo(assembly.Location)); - } + 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; + 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 358aab75dd31..b686067d30b8 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() - { - Configure(types => types.Clear()); - return This; - } - - /// - /// Adds a type to the collection. - /// - /// The type to append. - /// The builder. - public TBuilder Add() - where T : TItem + /// + /// Adds a type to the collection. + /// + /// The type to append. + /// The builder. + public TBuilder Add() + 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); - types.Add(type); - }); - return This; - } - - /// - /// Adds a type to the collection. - /// - /// The type to append. - /// The builder. - public TBuilder Add(Type type) + types.Remove(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)) { - 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) + 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 => { - Configure(list => + foreach (Type type in types) { - foreach (var type in types) + // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast + EnsureType(type, "register"); + 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; - } - - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove(Type type) + types.Remove(type); + } + }); + return This; + } + + /// + /// 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; - } - - /// - /// 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 + 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 + { + Configure(types => { - Configure(types => + Type typeReplaced = typeof(TReplaced); + Type type = typeof(T); + if (typeReplaced == type) + { + return; + } + + var index = types.IndexOf(typeReplaced); + if (index < 0) { - 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) + 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 => { - 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 40ce3d8a4628..072a9d99e300 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 HashSet(); + private readonly HashSet _types = new(); - protected abstract TBuilder This { get; } + 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 TCollection CreateCollection(IServiceProvider factory) + => factory.CreateInstance(CreateItemsFactory()); - public TBuilder Add(Type type) - { - _types.Add(Validate(type, "add")); - return This; - } + public void RegisterWith(IServiceCollection services) + => services.Add(new ServiceDescriptor(typeof(TCollection), CreateCollection, ServiceLifetime.Singleton)); - public TBuilder Add() - { - Add(typeof(T)); - return This; - } + public TBuilder Add(Type type) + { + _types.Add(Validate(type, "add")); + return This; + } - public TBuilder Add(IEnumerable types) + private static Type Validate(Type type, string action) + { + if (!typeof(TConstraint).IsAssignableFrom(type)) { - foreach (var type in types) - { - Add(type); - } - - return This; + throw new InvalidOperationException( + $"Cannot {action} type {type.FullName} as it does not inherit from/implement {typeof(TConstraint).FullName}."); } - public TBuilder Remove(Type type) - { - _types.Remove(Validate(type, "remove")); - return This; - } + return type; + } + + public TBuilder Add() + { + Add(typeof(T)); + return This; + } - public TBuilder Remove() + public TBuilder Add(IEnumerable types) + { + foreach (Type type in types) { - Remove(typeof(T)); - return This; + Add(type); } - public TCollection CreateCollection(IServiceProvider factory) - => factory.CreateInstance(CreateItemsFactory()); + return This; + } - public void RegisterWith(IServiceCollection services) - => services.Add(new ServiceDescriptor(typeof(TCollection), CreateCollection, ServiceLifetime.Singleton)); + public TBuilder Remove(Type type) + { + _types.Remove(Validate(type, "remove")); + return This; + } - // used to resolve a Func> parameter - private Func> CreateItemsFactory() => () => _types; + 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 dfeac6a73126..3ac826880ced 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 Umbraco.Cms.Core.Configuration.UmbracoSettings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing -{ +namespace Umbraco.Cms.Core.Composing; - /// - public class TypeFinder : ITypeFinder +/// +public class TypeFinder : ITypeFinder +{ + // 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 = { - 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(); - - private readonly ITypeFinderConfig? _typeFinderConfig; - // used for benchmark tests - internal bool QueryWithReferencingAssemblies { get; set; } = true; - - public TypeFinder(ILogger logger, IAssemblyProvider assemblyProvider, ITypeFinderConfig? typeFinderConfig = null) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _assemblyProvider = assemblyProvider; - _typeFinderConfig = typeFinderConfig; - } - - private string[]? _assembliesAcceptingLoadExceptions = null; + "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 static readonly ConcurrentDictionary TypeNamesCache = new(); + + 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; + } - private string[] AssembliesAcceptingLoadExceptions + /// + public IEnumerable AssembliesToScan + { + get { - get + lock (_localFilteredAssemblyCacheLocker) { - if (_assembliesAcceptingLoadExceptions is not null) + if (_localFilteredAssemblyCache != null) { - return _assembliesAcceptingLoadExceptions; + return _localFilteredAssemblyCache; } - _assembliesAcceptingLoadExceptions = - _typeFinderConfig?.AssembliesAcceptingLoadExceptions.Where(x => !x.IsNullOrWhiteSpace()).ToArray() ?? - Array.Empty(); - - return _assembliesAcceptingLoadExceptions; + IEnumerable assemblies = GetFilteredAssemblies(null, KnownAssemblyExclusionFilter); + _localFilteredAssemblyCache = new HashSet(assemblies); + return _localFilteredAssemblyCache; } } + } + // used for benchmark tests + internal bool QueryWithReferencingAssemblies { get; set; } = true; - private bool AcceptsLoadExceptions(Assembly a) + private string[] AssembliesAcceptingLoadExceptions + { + get { - 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 (_assembliesAcceptingLoadExceptions is not null) { - 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 - }); + 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; - private IEnumerable GetAllAssemblies() => _assemblyProvider.Assemblies; + return GetClassesWithBaseType(assignTypeFrom, assemblyList, onlyConcreteClasses, - /// - public IEnumerable AssembliesToScan - { - get - { - lock (_localFilteredAssemblyCacheLocker) - { - if (_localFilteredAssemblyCache != null) - return _localFilteredAssemblyCache; + // the additional filter will ensure that any found types also have the attribute applied. + t => t.GetCustomAttributes(attributeType, false).Any()); + } - var assemblies = GetFilteredAssemblies(null, KnownAssemblyExclusionFilter); - _localFilteredAssemblyCache = new HashSet(assemblies); - return _localFilteredAssemblyCache; - } - } - } + /// + /// 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 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) + 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)) { - 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); + return Type.GetType(name); } - // 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; + // 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)); + } - 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()); - } + #region Private methods - /// - /// 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; + // borrowed from aspnet System.Web.UI.Util + private static bool TypeNameContainsAssembly(string typeName) => CommaIndexInTypeName(typeName) > 0; - return GetClassesWithBaseType(assignTypeFrom, assemblyList, onlyConcreteClasses); + private bool AcceptsLoadExceptions(Assembly a) + { + if (AssembliesAcceptingLoadExceptions.Length == 0) + { + return false; } - /// - /// 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) + if (AssembliesAcceptingLoadExceptions.Length == 1 && AssembliesAcceptingLoadExceptions[0] == "*") { - var assemblyList = assemblies ?? AssembliesToScan; - - return GetClassesWithAttribute(attributeType, assemblyList, onlyConcreteClasses); + return true; } - /// - /// Returns a Type for the string type name - /// - /// - /// - public virtual Type? GetTypeByName(string name) + 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 + } - //NOTE: This will not find types in dynamic assemblies unless those assemblies are already loaded - //into the appdomain. - + if (pattern.Length == name?.Length) + { + return pattern.InvariantEquals(name); // same length, must be identical + } - // This is exactly what the BuildManager does, if the type is an assembly qualified type - // name it will find it. - if (TypeNameContainsAssembly(name)) + if (pattern[pattern.Length] != '.') { - return Type.GetType(name); + return false; // pattern is shorter than name, must end with dot } - // 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)); + 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(); } - #region Private methods + 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 bool TypeNameContainsAssembly(string typeName) + // borrowed from aspnet System.Web.UI.Util + private static int CommaIndexInTypeName(string typeName) + { + var num1 = typeName.LastIndexOf(','); + if (num1 < 0) { - return CommaIndexInTypeName(typeName) > 0; + return -1; } - // borrowed from aspnet System.Web.UI.Util - private static int CommaIndexInTypeName(string typeName) + var num2 = typeName.LastIndexOf(']'); + if (num2 > num1) { - var num1 = typeName.LastIndexOf(','); - if (num1 < 0) - return -1; - var num2 = typeName.LastIndexOf(']'); - if (num2 > num1) - return -1; - return typeName.IndexOf(',', num2 + 1); + return -1; } - private IEnumerable GetClassesWithAttribute( - Type attributeType, - IEnumerable assemblies, - bool onlyConcreteClasses) + 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) { - if (typeof(Attribute).IsAssignableFrom(attributeType) == false) - throw new ArgumentException("Type " + attributeType + " is not an Attribute type."); + 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 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); + var stack = new Stack(); + stack.Push(attributeType.Assembly); - if (!QueryWithReferencingAssemblies) + if (!QueryWithReferencingAssemblies) + { + foreach (Assembly a in candidateAssemblies) { - foreach (var a in candidateAssemblies) - stack.Push(a); + stack.Push(a); } + } - while (stack.Count > 0) - { - var assembly = stack.Pop(); + while (stack.Count > 0) + { + Assembly assembly = stack.Pop(); - IReadOnlyList? assemblyTypes = null; - if (assembly != attributeType.Assembly || attributeAssemblyIsCandidate) + IReadOnlyList? assemblyTypes = null; + if (assembly != attributeType.Assembly || attributeAssemblyIsCandidate) + { + // get all assembly types that can be assigned to baseType + try { - // 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 + assemblyTypes = GetTypesWithFormattedException(assembly) + .ToList(); // in try block } - - if (assembly != attributeType.Assembly && assemblyTypes?.Where(attributeType.IsAssignableFrom).Any() == false) + 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) + if (QueryWithReferencingAssemblies) + { + foreach (Assembly referencing in TypeHelper.GetReferencingAssemblies(assembly, candidateAssemblies)) { - foreach (var referencing in TypeHelper.GetReferencingAssemblies(assembly, candidateAssemblies)) - { - candidateAssemblies.Remove(referencing); - stack.Push(referencing); - } + 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(); + 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); + var stack = new Stack(); + stack.Push(baseType.Assembly); - if (!QueryWithReferencingAssemblies) + if (!QueryWithReferencingAssemblies) + { + foreach (Assembly a in candidateAssemblies) { - foreach (var a in candidateAssemblies) - stack.Push(a); + stack.Push(a); } + } - while (stack.Count > 0) - { - var assembly = stack.Pop(); + 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) + // get all assembly types that can be assigned to baseType + IReadOnlyList? assemblyTypes = null; + if (assembly != baseType.Assembly || baseTypeAssemblyIsCandidate) + { + try { - 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 + assemblyTypes = GetTypesWithFormattedException(assembly) + .Where(baseType.IsAssignableFrom) + .ToList(); // in try block } - - if (assembly != baseType.Assembly && (assemblyTypes?.All(x => x.IsSealed) ?? false)) + 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) + if (QueryWithReferencingAssemblies) + { + foreach (Assembly referencing in TypeHelper.GetReferencingAssemblies(assembly, candidateAssemblies)) { - foreach (var referencing in TypeHelper.GetReferencingAssemblies(assembly, candidateAssemblies)) - { - candidateAssemblies.Remove(referencing); - stack.Push(referencing); - } + candidateAssemblies.Remove(referencing); + stack.Push(referencing); } } + } - return types; + return types; + } + + private IEnumerable GetTypesWithFormattedException(Assembly a) + { + // if the assembly is dynamic, do not try to scan it + if (a.IsDynamic) + { + return Enumerable.Empty(); } - private IEnumerable GetTypesWithFormattedException(Assembly a) + var getAll = a.GetCustomAttribute() == null; + + try { - //if the assembly is dynamic, do not try to scan it - if (a.IsDynamic) - return Enumerable.Empty(); + // 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(); + } - var getAll = a.GetCustomAttribute() == null; + // GetExportedTypes *can* throw TypeLoadException! + catch (TypeLoadException ex) + { + var sb = new StringBuilder(); + AppendCouldNotLoad(sb, a, getAll); + AppendLoaderException(sb, ex); - 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()); + } - // 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 + // GetTypes throws ReflectionTypeLoadException + catch (ReflectionTypeLoadException rex) + { + var sb = new StringBuilder(); + AppendCouldNotLoad(sb, a, getAll); + foreach (Exception loaderException in rex.LoaderExceptions.WhereNotNull()) { - var sb = new StringBuilder(); - AppendCouldNotLoad(sb, a, getAll); - foreach (var loaderException in rex.LoaderExceptions.WhereNotNull()) - AppendLoaderException(sb, loaderException); + AppendLoaderException(sb, loaderException); + } - var ex = new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString()); + var ex = new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString()); - // rethrow with new message, unless accepted - if (AcceptsLoadExceptions(a) == false) - throw ex; + // rethrow with new message, unless accepted + if (AcceptsLoadExceptions(a) == false) + { + throw ex; + } - // log a warning, and return what we can - lock (_notifiedLoadExceptionAssemblies) + // log a warning, and return what we can + lock (_notifiedLoadExceptionAssemblies) + { + if (a.FullName is not null && _notifiedLoadExceptionAssemblies.Contains(a.FullName) == false) { - 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); - } + _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:"); + 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); - } + private static void AppendLoaderException(StringBuilder sb, Exception loaderException) + { + sb.Append(". "); + sb.Append(loaderException.GetType().FullName); - sb.Append(": "); - sb.Append(loaderException.Message); - sb.AppendLine(); + if (loaderException is TypeLoadException tloadex) + { + sb.Append(" on "); + sb.Append(tloadex.TypeName); } - #endregion - + 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 4b5271039fb0..2fd9283500a6 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; + private readonly TypeFinderSettings _settings; + private IEnumerable? _assembliesAcceptingLoadExceptions; - public TypeFinderConfig(IOptions settings) => _settings = settings.Value; + public TypeFinderConfig(IOptions settings) => _settings = settings.Value; - public IEnumerable AssembliesAcceptingLoadExceptions + 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 adb920b64a0e..c67d9357164f 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 08893732a8fe..6cb5426f77fd 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]; - + if (obj is null) + { + return null; + } + Type type = obj.GetType(); - /// - /// 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 (type.IsGenericType) { - if (obj is null) - { - return null; - } + Type genericTypeDef = type.GetGenericTypeDefinition(); - var type = obj.GetType(); + if (genericTypeDef == typeof(IEnumerable<>) + || genericTypeDef == typeof(ICollection<>) + || genericTypeDef == typeof(Collection<>) + || genericTypeDef == typeof(IList<>) + || genericTypeDef == typeof(List<>) - if (type.IsGenericType) + // this will occur when Linq is used and we get the odd WhereIterator or DistinctIterators since those are special iterator types + || obj is IEnumerable) { - 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 it is a IEnumerable<>, IList or ICollection<> we'll use a List<> + Type genericType = typeof(List<>).MakeGenericType(type.GetGenericArguments()); - 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); - } + // 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) + if (type.IsArray) { - return m.GetBaseDefinition().DeclaringType != m.DeclaringType; + // if its an array, we'll use a List<> + Type? 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); + } } - /// - /// 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; + 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 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(); + /// + /// 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; } - /// - /// Determines if an assembly references another assembly. - /// - /// - /// - /// - public static bool HasReference(Assembly assembly, string name) + // 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()) { - // ReSharper disable once LoopCanBeConvertedToQuery - no! - foreach (var a in assembly.GetReferencedAssemblies()) + if (string.Equals(a.Name, name, StringComparison.Ordinal)) { - if (string.Equals(a.Name, name, StringComparison.Ordinal)) return true; + return true; } - return false; } - /// - /// Returns true if the type is a class and is not static - /// - /// - /// - public static bool IsNonStaticClass(Type t) + 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 t.IsClass && IsStaticClass(t) == false; + return Attempt.Fail(); } - /// - /// 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) + if (types.Length == 1) { - return type.IsAbstract && type.IsSealed; + return Attempt.Succeed(types[0]); } - /// - /// 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) + foreach (Type curr in types) { - if (types.Length == 0) - return Attempt.Fail(); + IEnumerable others = types.Except(new[] { curr }); - if (types.Length == 1) - return Attempt.Succeed(types[0]); + // is the current type a common denominator for all others ? + var isBase = others.All(curr.IsAssignableFrom); - foreach (var curr in types) + // if this type is the base for all others + if (isBase) { - var others = types.Except(new[] {curr}); + return Attempt.Succeed(curr); + } + } - //is the current type a common denominator for all others ? - var isBase = others.All(curr.IsAssignableFrom); + return Attempt.Fail(); + } - //if this type is the base for all others - if (isBase) - { - return Attempt.Succeed(curr); - } - } + /// + /// 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); - 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. + public static bool IsTypeAssignableFrom(Type implementation) => + IsTypeAssignableFrom(typeof(TContract), 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. - /// - /// true if [is type assignable from] [the specified contract]; otherwise, false. - /// - public static bool IsTypeAssignableFrom(Type contract, Type? 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 contract.IsAssignableFrom(implementation); + throw new ArgumentNullException(nameof(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) - { - return IsTypeAssignableFrom(typeof(TContract), implementation); - } + return IsTypeAssignableFrom(implementation.GetType()); + } - /// - /// 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()); - } + /// + /// 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 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) => + IsValueType(implementation) || implementation.IsEnum || implementation == typeof(string); - /// - /// 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) => + 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()); - /// - /// 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) + /// + /// 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) { - return CachedDiscoverableProperties(type, mustRead, mustWrite, includeIndexed) - .FirstOrDefault(x => caseSensitive ? (x.Name == name) : x.Name.InvariantEquals(name)); + return false; } - /// - /// Gets (and caches) discoverable in the current for a given . - /// - /// The source. - /// - public static FieldInfo[] CachedDiscoverableFields(Type type) + // must have the same generic type definition + Type implDef = implementation.GetGenericTypeDefinition(); + Type contDef = contract.GetGenericTypeDefinition(); + if (implDef != contDef) { - return GetFieldsCache.GetOrAdd( - type, - x => type - .GetFields(BindingFlags.Public | BindingFlags.Instance) - .Where(y => y.IsInitOnly == false) - .ToArray()); + return false; } - /// - /// 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) + // must have the same number of generic arguments + Type[] implArgs = implementation.GetGenericArguments(); + Type[] contArgs = contract.GetGenericArguments(); + if (implArgs.Length != contArgs.Length) { - 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()); + return false; } - #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) + // 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++) { - // 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++) + const bool variance = false; // classes are NOT invariant + if (MatchType(implArgs[i], contArgs[i], bindings, variance) == false) { - const bool variance = false; // classes are NOT invariant - if (MatchType(implArgs[i], contArgs[i], bindings, variance) == false) - return false; + return false; } - - return true; } - public static bool MatchType(Type implementation, Type contract) - { - return MatchType(implementation, contract, new Dictionary()); - } + return true; + } - public static bool MatchType(Type implementation, Type contract, IDictionary bindings, bool variance = true) + public static bool MatchType(Type implementation, Type contract, IDictionary bindings, bool variance = true) + { + if (contract.IsGenericType) { - 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 + // 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; + // try to match implementation against contract + if (MatchGeneric(implementation, contract, bindings)) + { + return true; + } - // if no variance, fail - if (variance == false) return false; + // 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) + // try to match an ancestor of implementation against contract + Type? t = implementation.BaseType; + while (t != null) + { + if (MatchGeneric(t, contract, bindings)) { - if (MatchGeneric(t, contract, bindings)) return true; - t = t.BaseType; + return true; } - // try to match an interface of implementation against contract - return implementation.GetInterfaces().Any(i => MatchGeneric(i, contract, bindings)); + t = t.BaseType; } - if (contract.IsGenericParameter) - { - // eg - - if (bindings.ContainsKey(contract.Name)) - { - // already bound: ensure it's compatible - return bindings[contract.Name] == implementation; - } + // try to match an interface of implementation against contract + return implementation.GetInterfaces().Any(i => MatchGeneric(i, contract, bindings)); + } - // not already bound: bind - bindings[contract.Name] = implementation; - return true; + if (contract.IsGenericParameter) + { + // eg + if (bindings.ContainsKey(contract.Name)) + { + // already bound: ensure it's compatible + return bindings[contract.Name] == implementation; } - // 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 == + // not already bound: bind + bindings[contract.Name] = implementation; + return true; + } - if (implementation == contract) return true; - if (contract.IsClass && implementation.IsClass && implementation.IsSubclassOf(contract)) return true; - if (contract.IsInterface && implementation.GetInterfaces().Contains(contract)) 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; + } - return false; + if (contract.IsClass && implementation.IsClass && implementation.IsSubclassOf(contract)) + { + return true; } - #endregion + 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 6f4d81fc34bd..7fadd102da53 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.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; + + /// + /// 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) + { + } + /// - /// 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, + 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 - { - private readonly ILogger _logger; + // internal for tests + [Obsolete("This will be removed in a future version.")] + public IEnumerable AssembliesToScan => _assemblies ??= TypeFinder.AssembliesToScan; - private readonly Dictionary _types = new (); - private readonly object _locko = new (); + /// + /// Gets the type lists. + /// + /// For unit tests. + // internal for tests + [Obsolete("This will be removed in a future version.")] + public IEnumerable TypeLists => _types.Values; - private IEnumerable? _assemblies; + /// + /// Sets a type list. + /// + /// For unit tests. + // internal for tests + [Obsolete("This will be removed in a future version.")] + public void AddTypeList(TypeList typeList) + { + Type tobject = typeof(object); // CompositeTypeTypeKey does not support null values + _types[new CompositeTypeTypeKey(typeList.BaseType ?? tobject, typeList.AttributeType ?? tobject)] = typeList; + } - /// - /// 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) - { - } + #region Get Assembly Attributes - /// - /// 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) + /// + /// 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)); } - 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; - } + return AssembliesToScan.SelectMany(a => attributeTypes.SelectMany(at => a.GetCustomAttributes(at))).ToList(); + } - /// - /// Returns the underlying - /// - // ReSharper disable once MemberCanBePrivate.Global - public ITypeFinder TypeFinder { get; } + #endregion - /// - /// 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; + #region Cache - /// - /// Gets the type lists. - /// - /// For unit tests. - // internal for tests - [Obsolete("This will be removed in a future version.")] - public IEnumerable TypeLists => _types.Values; + // internal for tests + [Obsolete("This will be removed in a future version.")] + public Attempt> TryGetCached(Type baseType, Type attributeType) => + Attempt>.Fail(); - /// - /// 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; - } + // internal for tests + [Obsolete("This will be removed in a future version.")] + public Dictionary<(string, string), IEnumerable>? ReadCache() => null; - #region Cache + // 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 Attempt> TryGetCached(Type baseType, Type attributeType) - { - return Attempt>.Fail(); - } + // internal for tests + [Obsolete("This will be removed in a future version.")] + public void WriteCache() + { + } - // internal for tests - [Obsolete("This will be removed in a future version.")] - public Dictionary<(string, string), IEnumerable>? ReadCache() => null; + /// + /// 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() + { + } - // internal for tests - [Obsolete("This will be removed in a future version.")] - public string? GetTypesListFilePath() => null; + #endregion - // internal for tests - [Obsolete("This will be removed in a future version.")] - public void WriteCache() - { - } + #region Get Types - /// - /// 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() + /// + /// 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."); } - #endregion + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; - #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 not IDiscoverable, directly get types + if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) { - if (attributeTypes == null) - throw new ArgumentNullException(nameof(attributeTypes)); + // warn + _logger.LogDebug( + "Running a full, " + (cache ? string.Empty : "non-") + + "cached, scan for non-discoverable type {TypeName} (slow).", + typeof(T).FullName); - return AssembliesToScan.SelectMany(a => attributeTypes.SelectMany(at => a.GetCustomAttributes(at))).ToList(); + return GetTypesInternal( + typeof(T), + null, + () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), + "scanning assemblies", + cache); } - #endregion - - #region Get Types + // get IDiscoverable and always cache + IEnumerable discovered = GetTypesInternal( + typeof(IDiscoverable), + null, + () => TypeFinder.FindClassesOfType(AssembliesToScan), + "scanning assemblies", + true); - /// - /// 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) + // warn + if (!cache) { - if (_logger == null) - { - throw new InvalidOperationException("Cannot get types from a test/blank type loader."); - } + _logger.LogDebug( + "Running a non-cached, filter for discoverable type {TypeName} (slowish).", + typeof(T).FullName); + } - // do not cache anything from specific assemblies - cache &= specificAssemblies == null; + // 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); + } - // 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); - } + /// + /// 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."); + } - // get IDiscoverable and always cache - var discovered = GetTypesInternal( - typeof(IDiscoverable), null, - () => TypeFinder.FindClassesOfType(AssembliesToScan), - "scanning assemblies", - true); + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; - // warn - if (!cache) - { - _logger.LogDebug("Running a non-cached, filter for discoverable type {TypeName} (slowish).", typeof(T).FullName); - } + // if not IDiscoverable, directly get types + if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) + { + _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); - // filter the cached discovered types (and maybe cache the result) return GetTypesInternal( - typeof(T), null, - () => discovered - .Where(x => typeof(T).IsAssignableFrom(x)), - "filtering IDiscoverable", + typeof(T), + typeof(TAttribute), + () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), + "scanning assemblies", 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 + // get IDiscoverable and always cache + IEnumerable discovered = GetTypesInternal( + typeof(IDiscoverable), + null, + () => TypeFinder.FindClassesOfType(AssembliesToScan), + "scanning assemblies", + true); + + // warn + if (!cache) { - if (_logger == null) - { - throw new InvalidOperationException("Cannot get types from a test/blank type loader."); - } + _logger.LogDebug( + "Running a non-cached, filter for discoverable type {TypeName} / attribute {AttributeName} (slowish).", + typeof(T).FullName, + typeof(TAttribute).FullName); + } - // do not cache anything from specific assemblies - cache &= specificAssemblies == null; + // 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); + } - // 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); + /// + /// 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."); + } - return GetTypesInternal( - typeof(T), typeof(TAttribute), - () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), - "scanning assemblies", - cache); - } + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; - // get IDiscoverable and always cache - var discovered = GetTypesInternal( - typeof(IDiscoverable), null, - () => TypeFinder.FindClassesOfType(AssembliesToScan), - "scanning assemblies", - true); + if (!cache) + { + _logger.LogDebug( + "Running a full, non-cached, scan for types / attribute {AttributeName} (slow).", + typeof(TAttribute).FullName); + } - // warn - if (!cache) - { - _logger.LogDebug("Running a non-cached, filter for discoverable type {TypeName} / attribute {AttributeName} (slowish).", typeof(T).FullName, typeof(TAttribute).FullName); - } + return GetTypesInternal( + typeof(object), + typeof(TAttribute), + () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), + "scanning assemblies", + cache); + } - // 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); - } + private static string GetName(Type? baseType, Type? attributeType) + { + var s = attributeType == null ? string.Empty : "[" + attributeType + "]"; + s += baseType; + return s; + } - /// - /// 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 + 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) { - 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; + return GetTypesInternalLocked(baseType, attributeType, finder, action, cache); + } + } - if (!cache) - { - _logger.LogDebug("Running a full, non-cached, scan for types / attribute {AttributeName} (slow).", typeof(TAttribute).FullName); - } + 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; - return GetTypesInternal( - typeof(object), typeof(TAttribute), - () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), - "scanning assemblies", - cache); + if (cache) + { + _types.TryGetValue(listKey, out typeList); // else null } - private IEnumerable GetTypesInternal( - Type baseType, - Type? attributeType, - Func> finder, - string action, - bool cache) + // if caching and found, return + if (typeList != null) { - // 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); - } + // 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; } - private static string GetName(Type? baseType, Type? attributeType) + // else proceed, + typeList = new TypeList(baseType, attributeType); + + // 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)); + + foreach (Type t in finder()) { - var s = attributeType == null ? string.Empty : ("[" + attributeType + "]"); - s += baseType; - return s; + typeList.Add(t); } - private IEnumerable GetTypesInternalLocked( - Type? baseType, - Type? attributeType, - Func> finder, - string action, - bool cache) + // if we are to cache the results, do so + if (cache) { - // 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; - - if (cache) - { - _types.TryGetValue(listKey, out typeList); // else null - } - - // if caching and found, return - if (typeList != null) + var added = _types.ContainsKey(listKey) == false; + if (added) { - // 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; + _types[listKey] = typeList; } - // else proceed, - typeList = new TypeList(baseType, attributeType); + _logger.LogDebug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant()); + } + else + { + _logger.LogDebug("Got {TypeName}.", GetName(baseType, attributeType)); + } - // 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)); + return typeList.Types; + } - foreach (var t in finder()) - { - typeList.Add(t); - } + #endregion - // if we are to cache the results, do so - if (cache) - { - var added = _types.ContainsKey(listKey) == false; - if (added) - { - _types[listKey] = typeList; - } + #region Nested classes and stuff - _logger.LogDebug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant()); - } - else - { - _logger.LogDebug("Got {TypeName}.", 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 + { + private readonly HashSet _types = new(); - return typeList.Types; + public TypeList(Type? baseType, Type? attributeType) + { + BaseType = baseType; + AttributeType = attributeType; } - #endregion + public Type? BaseType { get; } - #region Nested classes and stuff + public Type? AttributeType { get; } /// - /// Represents a list of types obtained by looking for types inheriting/implementing a - /// specified type, and/or marked with a specified attribute type. + /// Gets the types. /// - public sealed class TypeList - { - private readonly HashSet _types = new HashSet(); + public IEnumerable Types => _types; - public TypeList(Type? baseType, Type? attributeType) + /// + /// Adds a type. + /// + public void Add(Type type) + { + if (BaseType?.IsAssignableFrom(type) == false) { - BaseType = baseType; - AttributeType = attributeType; + throw new ArgumentException( + "Base type " + BaseType + " is not assignable from type " + type + ".", + nameof(type)); } - public Type? BaseType { get; } - public Type? AttributeType { get; } + _types.Add(type); + } + } - /// - /// 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() + { + } - /// - /// Gets the types. - /// - public IEnumerable Types => _types; + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public CachedTypeNotFoundInFileException(string message) + : base(message) + { } /// - /// Represents the error that occurs when a type was not found in the cache type list with the specified TypeResolutionKind. + /// Initializes a new instance of the class. /// - /// - [Serializable] - internal class CachedTypeNotFoundInFileException : 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 CachedTypeNotFoundInFileException(string message, Exception innerException) + : base(message, innerException) { - /// - /// 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 + /// + /// 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 eec2adc63776..740921974db2 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 1225abca0c75..a69ca4636e90 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 1eafcce9e01c..56b714d35a0e 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; } - - private readonly Dictionary _customWeights = new Dictionary(); - - /// - /// Clears all types in the collection. - /// - /// The builder. - public TBuilder Clear() - { - Configure(types => types.Clear()); - return This; - } + Configure(types => types.Clear()); + return This; + } - /// - /// Adds a type to the collection. - /// - /// The type to add. - /// The builder. - public TBuilder Add() - where T : TItem + /// + /// Adds a type to the collection. + /// + /// The type to add. + /// The builder. + public TBuilder Add() + where T : TItem + { + Configure(types => { - 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) { - EnsureType(type, "register"); - if (types.Contains(type) == false) types.Add(type); - }); - return This; - } + types.Add(type); + } + }); + return This; + } - /// - /// Adds types to the collection. - /// - /// The types to add. - /// The builder. - public TBuilder Add(IEnumerable types) + /// + /// Adds types to the collection. + /// + /// The types to add. + /// The builder. + public TBuilder Add(IEnumerable types) + { + Configure(list => { - Configure(list => + foreach (Type type in types) { - foreach (var type in types) + // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast + EnsureType(type, "register"); + 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; - } - - /// - /// 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; - } + types.Remove(type); + } + }); + return This; + } - protected override IEnumerable GetRegisteringTypes(IEnumerable types) - { - var list = types.ToList(); - list.Sort((t1, t2) => GetWeight(t1).CompareTo(GetWeight(t2))); - return list; - } + /// + /// 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; + } - public virtual int DefaultWeight { get; set; } = 100; + 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) + protected virtual int GetWeight(Type type) + { + if (_customWeights.ContainsKey(type)) { - if (_customWeights.ContainsKey(type)) - return _customWeights[type]; - var attr = type.GetCustomAttributes(typeof(WeightAttribute), false).OfType().SingleOrDefault(); - return attr?.Weight ?? DefaultWeight; + return _customWeights[type]; } + + 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 e69de29bb2d1..8b137891791f 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 829d19bb5392..cd256e1b4561 100644 --- a/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs +++ b/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs @@ -6,14 +6,14 @@ 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 void Configure(string name, ConnectionStrings options) options.Name = name; options.ConnectionString = _configuration.GetUmbracoConnectionString(name, out string? providerName); - options.ProviderName = providerName ?? ConnectionStrings.DefaultProviderName; + options.ProviderName = providerName ?? + ConnectionStrings.DefaultProviderName; } } diff --git a/src/Umbraco.Core/Configuration/ContentSettingsExtensions.cs b/src/Umbraco.Core/Configuration/ContentSettingsExtensions.cs index c8f5c4198844..315cee462780 100644 --- a/src/Umbraco.Core/Configuration/ContentSettingsExtensions.cs +++ b/src/Umbraco.Core/Configuration/ContentSettingsExtensions.cs @@ -1,32 +1,28 @@ -using System.Linq; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ContentSettingsExtensions { - 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) - { - return contentSettings.AllowedUploadFiles.Any(x => x.InvariantEquals(extension)) || - (contentSettings.AllowedUploadFiles.Any() == false && - contentSettings.DisallowedUploadFiles.Any(x => x.InvariantEquals(extension)) == false); - } + /// + /// 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) - { - var autoFillConfigs = contentSettings.Imaging.AutoFillImageProperties; - return autoFillConfigs?.FirstOrDefault(x => x.Alias == propertyTypeAlias); - } + /// + /// 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 b6b9f067b9a6..096eac6fe013 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; - } + InformationalVersion = entryAssembly + ?.GetCustomAttribute() + ?.InformationalVersion ?? string.Empty; + } - public string Name { get; } + public string Name { get; } - public string InformationalVersion { get; } - } + public string InformationalVersion { get; } } diff --git a/src/Umbraco.Core/Configuration/Extensions/HealthCheckSettingsExtensions.cs b/src/Umbraco.Core/Configuration/Extensions/HealthCheckSettingsExtensions.cs index 765525298187..bbf8c67db519 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 2b22b0f28bb3..2f49bfd146cd 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 +namespace Umbraco.Extensions; + +public static class GlobalSettingsExtensions { - 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) { - 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) - { - if (_mvcArea != null) return _mvcArea; - - _mvcArea = globalSettings.GetUmbracoMvcAreaNoCache(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) + { return _mvcArea; } - internal static string GetUmbracoMvcAreaNoCache(this GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) - { - var path = string.IsNullOrEmpty(globalSettings.UmbracoPath) - ? string.Empty - : hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath); + _mvcArea = globalSettings.GetUmbracoMvcAreaNoCache(hostingEnvironment); - if (path.IsNullOrWhiteSpace()) - throw new InvalidOperationException("Cannot create an MVC Area path without the umbracoPath specified"); + 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()) + { + 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(); + // 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 27d6820399aa..44c9c37dfd8a 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 +namespace Umbraco.Cms.Core.Configuration.Grid; + +public class GridConfig : IGridConfig { - 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 GridConfig( + AppCaches appCaches, + IManifestParser manifestParser, + IJsonSerializer jsonSerializer, + IHostingEnvironment hostingEnvironment, + ILoggerFactory loggerFactory) + => EditorsConfig = + new GridEditorsConfig(appCaches, hostingEnvironment, manifestParser, jsonSerializer, loggerFactory.CreateLogger()); - public IGridEditorsConfig EditorsConfig { get; } - } + public IGridEditorsConfig EditorsConfig { get; } } diff --git a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs index db5d669ce93a..11ae329192fb 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.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 IManifestParser _manifestParser; + private readonly AppCaches _appCaches; + private readonly IHostingEnvironment _hostingEnvironment; - private readonly IJsonSerializer _jsonSerializer; - private readonly ILogger _logger; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + private readonly IManifestParser _manifestParser; - public GridEditorsConfig(AppCaches appCaches, IHostingEnvironment hostingEnvironment, IManifestParser manifestParser,IJsonSerializer jsonSerializer, ILogger logger) - { - _appCaches = appCaches; - _hostingEnvironment = hostingEnvironment; - _manifestParser = manifestParser; - _jsonSerializer = jsonSerializer; - _logger = logger; - } + public GridEditorsConfig( + AppCaches appCaches, + IHostingEnvironment hostingEnvironment, + IManifestParser manifestParser, + IJsonSerializer jsonSerializer, + ILogger logger) + { + _appCaches = appCaches; + _hostingEnvironment = hostingEnvironment; + _manifestParser = manifestParser; + _jsonSerializer = jsonSerializer; + _logger = logger; + } - public IEnumerable Editors + public IEnumerable Editors + { + get { - 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); + 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); - } + try + { + 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); } + } + + // Read default from embedded file + else + { + Assembly assembly = GetType().Assembly; + Stream? resourceStream = assembly.GetManifestResourceStream( + "Umbraco.Cms.Core.EmbeddedResources.Grid.grid.editors.config.js"); - // add manifest editors, skip duplicates - foreach (var gridEditor in _manifestParser.CombinedManifest.GridEditors) + if (resourceStream is not null) { - if (editors.Contains(gridEditor) == false) editors.Add(gridEditor); + using var reader = new StreamReader(resourceStream, Encoding.UTF8); + var sourceString = reader.ReadToEnd(); + editors.AddRange(_jsonSerializer.Deserialize>(sourceString)!); } - - return editors; } - //cache the result if debugging is disabled - var result = _hostingEnvironment.IsDebugMode - ? GetResult() - : _appCaches.RuntimeCache.GetCacheItem>(typeof(GridEditorsConfig) + ".Editors",GetResult, TimeSpan.FromMinutes(10)); + // add manifest editors, skip duplicates + foreach (GridEditor gridEditor in _manifestParser.CombinedManifest.GridEditors) + { + if (editors.Contains(gridEditor) == false) + { + editors.Add(gridEditor); + } + } - return result!; + 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 d009eddd25d3..4dd11ee1fc1c 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 -{ - public interface IGridConfig - { - - IGridEditorsConfig EditorsConfig { get; } +namespace Umbraco.Cms.Core.Configuration.Grid; - } +public interface IGridConfig +{ + IGridEditorsConfig EditorsConfig { get; } } diff --git a/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs b/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs index bfd3f17cbfeb..5103e7a328f7 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 a49ae41d6ce3..e0d8c8f8d412 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 c99f90e5c91b..18ce8a5eca37 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 565d9fa47b48..bd3808ecd182 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 09ea5058df35..857b62bb2611 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 7bd8ab9ef2a8..451cf51bc309 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 acfe81ece9fa..e0e934f550dc 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; } + + /// + /// 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 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 for the password hash algorithm type. + /// + string HashAlgorithmType { get; } /// - /// Password configuration + /// Gets a value for the maximum failed access attempts before lockout. /// - public interface IPasswordConfiguration - { - /// - /// Gets a value for the minimum required length for the password. - /// - int RequiredLength { 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 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 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; } - } + /// + /// 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 e9842ee4cba9..4acbd1dbd872 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 4a1e65f13f6f..5547639b11ce 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 2758d9dabfb7..3672f28dae29 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 -{ - public interface IUmbracoVersion - { - /// - /// Gets the non-semantic version of the Umbraco code. - /// - Version Version { get; } +namespace Umbraco.Cms.Core.Configuration; - /// - /// Gets the semantic version comments of the Umbraco code. - /// - string Comment { get; } +public interface IUmbracoVersion +{ + /// + /// Gets the non-semantic version of the Umbraco code. + /// + Version Version { 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 semantic version comments of the Umbraco code. + /// + string Comment { get; } - /// - /// Gets the assembly file version of the Umbraco code. - /// - /// - /// The assembly version is the value of the . - /// - Version? AssemblyFileVersion { 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 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 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; } } diff --git a/src/Umbraco.Core/Configuration/IUserPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/IUserPasswordConfiguration.cs index db27103a67a5..c4f86232d309 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 696ec7900e9e..8be409fc2b8a 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 c7ce20454f1c..33471ced160f 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 646cd7ff9fed..3373b7a7786a 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 211b6b3d8311..5f42aac54523 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 aa82f69d2e2d..b743fdcdd281 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"; +public class SharedSecret +{ + private const string StaticHeaderName = "X-Authentication-Shared-Secret"; - [DefaultValue(StaticHeaderName)] - public string? HeaderName { get; set; } = StaticHeaderName; - public string? Value { get; set; } - } + [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 a74b0c0a8ba6..625033a82acb 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 +namespace Umbraco.Cms.Core.Configuration.Models; + +public class CharItem : IChar { - public class CharItem : IChar - { - /// - /// The character to replace - /// - public string Char { get; set; } = null!; + /// + /// The character to replace + /// + public string Char { get; set; } = null!; - /// - /// The replacement character - /// - public string Replacement { 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 f4703adf924c..a5161eca865c 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 19d636ed347e..74376a3ed2c4 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 6a6d3a8e61ec..415240e0176a 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 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 XPath. + /// + public string? ContentXPath { get; set; } + + /// + /// Gets a value indicating whether the field is populated. /// - public class ContentErrorPage : ValidatableEntryBase - { - /// - /// Gets or sets a value for the content Id. - /// - 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 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 HasContentKey => ContentKey != Guid.Empty; - - /// - /// 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!; - - internal override bool IsValid() => - base.IsValid() && - ((HasContentId ? 1 : 0) + (HasContentKey ? 1 : 0) + (HasContentXPath ? 1 : 0) == 1); - } + 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 HasContentXPath => !string.IsNullOrEmpty(ContentXPath); + + /// + /// 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; } diff --git a/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs index 2e109fe31009..4634f6efb9c6 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, - } - }; - - internal const string StaticImageFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp"; + 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, + }, + }; - /// - /// 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 c23eac75b2a5..ce5c3aebf319 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; - - /// - /// Gets or sets a value for the email address for notifications. - /// - public string? Email { get; set; } + 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 e6e5c7006fe7..f0532a720381 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 +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for content settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigContent)] +public class ContentSettings { - /// - /// Typed configuration options for content settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigContent)] - public class ContentSettings - { + internal const bool StaticResolveUrlsFromTextString = false; - internal const bool StaticResolveUrlsFromTextString = false; - internal const string StaticDefaultPreviewBadge = - @" + internal const string StaticDefaultPreviewBadge = + @"
Preview mode @@ -151,98 +149,97 @@ @keyframes umbraco-preview-badge--effect {{ "; - 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 imaging settings. - /// - public ContentImagingSettings Imaging { get; set; } = new ContentImagingSettings(); - - /// - /// 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 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 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 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 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 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; - - /// - /// Get or sets the model representing the global content version cleanup policy - /// - public ContentVersionCleanupPolicySettings ContentVersionCleanupPolicy { get; set; } = new ContentVersionCleanupPolicySettings(); - } + 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(); + + /// + /// 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 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 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 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 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 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 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(); } diff --git a/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs b/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs index bd460058eb3b..ed721382a963 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; - - /// - /// 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; + [DefaultValue(StaticEnableCleanup)] + public bool EnableCleanup { get; set; } = StaticEnableCleanup; - /// - /// 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 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; } diff --git a/src/Umbraco.Core/Configuration/Models/CoreDebugSettings.cs b/src/Umbraco.Core/Configuration/Models/CoreDebugSettings.cs index 58810a34625d..052d37c5fe3a 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; - - /// - /// Gets or sets a value indicating whether incompleted scopes should be logged. - /// - [DefaultValue(StaticLogIncompletedScopes)] - public bool LogIncompletedScopes { get; set; } = StaticLogIncompletedScopes; + [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 f1320a199faf..a083b164a8d2 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); - - /// - /// 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; + [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 91d293f272ce..80aefeae6eec 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"; - - /// - /// 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); + [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 a24ec5b92324..f055aca7abc5 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 0d48453071ed..ebf99c03ddc0 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 +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; + /// - /// Typed configuration options for exception filter settings. + /// Gets or sets a value indicating whether the exception filter is disabled. /// - [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; - } + [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 8d00d5819847..5351da317c02 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; + + /// + /// 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 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 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 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 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 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 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 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 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 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 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(); + /// - /// 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; - - /// - /// 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 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 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 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 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 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 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 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 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 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 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 DatabaseServerRegistrarSettings(); - - /// - /// 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 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 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 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 for the database server messenger settings. + /// + public DatabaseServerMessengerSettings DatabaseServerMessenger { get; set; } = new(); + + /// + /// 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 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 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 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 2fc621a4820d..c973f590250e 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; - - /// - /// Gets or sets a value indicating whether the health check notification method is enabled. - /// - [DefaultValue(StaticEnabled)] - public bool Enabled { get; set; } = StaticEnabled; + [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 6e082da19f5c..6e81c48c7cdf 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); + + /// + /// 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 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(); + /// - /// Typed configuration options for healthcheck notification settings. + /// Gets or sets a value for the collection of health checks that are disabled for notifications. /// - public class HealthChecksNotificationSettings - { - internal const bool StaticEnabled = false; - internal const string StaticPeriod = "1.00:00:00"; //TimeSpan.FromHours(24); - - /// - /// 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 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 checks that are disabled for notifications. - /// - public IEnumerable DisabledChecks { get; set; } = Enumerable.Empty(); - } + 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 0d232b9a9bb7..6ae79e974307 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 b608b5c1554e..01d028f883ac 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 8f5f47a566fd..2329c73d6600 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; - - /// - /// Gets or sets a value for the application virtual path. - /// - public string? ApplicationVirtualPath { get; set; } + 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 8a0a1658b22d..3bcf91b0be17 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 b3bdddc211d3..a433c5d3009d 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"; + + /// + /// 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 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; + /// - /// Typed configuration options for image cache settings. + /// Gets or sets a value for the image cache folder. /// - 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"; - - /// - /// 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 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. - /// - [DefaultValue(StaticCacheFolder)] - public string CacheFolder { get; set; } = StaticCacheFolder; - } + [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 ff02fdc5229e..dc4585bf9c7d 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; - - /// - /// Gets or sets a value for the maximim resize width. - /// - [DefaultValue(StaticMaxWidth)] - public int MaxWidth { get; set; } = StaticMaxWidth; + [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 fde303343c9c..8232746ead3e 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 c140463b4a27..8c18495d5576 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 377e893bbf52..25789b397bdb 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, + None, - /// - /// Only install the default data specified in the - /// - Values, + /// + /// Only install the default data specified in the + /// + Values, - /// - /// Install all default data, except that specified in the - /// - ExceptValues, + /// + /// Install all default data, except that specified in the + /// + ExceptValues, - /// - /// Install all default data. - /// - All - } + /// + /// Install all default data. + /// + All, +} +/// +/// Typed configuration options for installation of default data. +/// +public class InstallDefaultDataSettings +{ /// - /// Typed configuration options for installation of default data. + /// Gets or sets a value indicating whether to create default data on installation. /// - public class InstallDefaultDataSettings - { - /// - /// Gets or sets a value indicating whether to create default data on installation. - /// - public InstallDefaultDataOption InstallData { get; set; } = InstallDefaultDataOption.All; + 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(); - } + /// + /// 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 297e1dff87e0..64cd61ad2632 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"; - - /// - /// Gets or sets a value indicating whether the keep alive task is disabled. - /// - [DefaultValue(StaticDisableKeepAliveTask)] - public bool DisableKeepAliveTask { get; set; } = StaticDisableKeepAliveTask; + [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 c3909ed619b2..b44d70a46aca 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 = ""; - - /// - /// Gets the decryption algorithm. - /// - /// - /// Currently only AES is supported. This should include all machine keys generated by Umbraco. - /// - public string MachineKeyDecryption => "AES"; + /// + /// 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 2075921c3f9f..37b671926c55 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 +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); + /// - /// Typed configuration options for logging settings. + /// Gets or sets a value for the maximum age of a log file. /// - [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); - } + [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 5f06a850f16a..3b0e974c089b 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 fa4f0725f7e0..1e884a150f28 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; - - /// - [DefaultValue(StaticRequiredLength)] - public int RequiredLength { get; set; } = StaticRequiredLength; - - /// - [DefaultValue(StaticRequireNonLetterOrDigit)] - public bool RequireNonLetterOrDigit { get; set; } = StaticRequireNonLetterOrDigit; - - /// - [DefaultValue(StaticRequireDigit)] - public bool RequireDigit { get; set; } = StaticRequireDigit; - - /// - [DefaultValue(StaticRequireLowercase)] - public bool RequireLowercase { get; set; } = StaticRequireLowercase; - - /// - [DefaultValue(StaticRequireUppercase)] - public bool RequireUppercase { get; set; } = StaticRequireUppercase; - - /// - [DefaultValue(Constants.Security.AspNetCoreV3PasswordHashAlgorithmName)] - public string HashAlgorithmType { get; set; } = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName; - - /// - [DefaultValue(StaticMaxFailedAccessAttemptsBeforeLockout)] - public int MaxFailedAccessAttemptsBeforeLockout { get; set; } = StaticMaxFailedAccessAttemptsBeforeLockout; - } + 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(StaticRequireNonLetterOrDigit)] + public bool RequireNonLetterOrDigit { get; set; } = StaticRequireNonLetterOrDigit; + + /// + [DefaultValue(StaticRequireDigit)] + public bool RequireDigit { get; set; } = StaticRequireDigit; + + /// + [DefaultValue(StaticRequireLowercase)] + public bool RequireLowercase { get; set; } = StaticRequireLowercase; + + /// + [DefaultValue(StaticRequireUppercase)] + public bool RequireUppercase { get; set; } = StaticRequireUppercase; + + /// + [DefaultValue(Constants.Security.AspNetCoreV3PasswordHashAlgorithmName)] + public string HashAlgorithmType { get; set; } = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName; + + /// + [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 73d046de32cb..fdb7bac0ef82 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 - { - 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; + [DefaultValue(StaticModelsMode)] + public ModelsMode ModelsMode { get; set; } = Enum.Parse(StaticModelsMode); - /// - /// 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 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 + { + get => _flagOutOfDateModels; - /// - /// 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; } - } - /// - /// 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; + _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 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 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 8f889b10c3d4..0506ddb98b8d 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 ee41fc32d395..b88dbb5d0dce 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; + + /// + /// 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 paging size to use for nucache SQL queries. + /// + [DefaultValue(StaticSqlPageSize)] + public int SqlPageSize { get; set; } = StaticSqlPageSize; + /// - /// Typed configuration options for NuCache settings. + /// The size to use for nucache Kit batches. Higher value means more content loaded into memory at a time. /// - [UmbracoOptions(Constants.Configuration.ConfigNuCache)] - public class NuCacheSettings - { - internal const string StaticNuCacheSerializerType = "MessagePack"; - internal const int StaticSqlPageSize = 1000; - internal const int StaticKitBatchSize = 1; - - /// - /// 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 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; - - public bool UnPublishedContentCompression { get; set; } = false; - } + [DefaultValue(StaticKitBatchSize)] + public int KitBatchSize { get; set; } = StaticKitBatchSize; + + 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 27968fdcd2ad..ee48d5a64258 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; - - /// - /// 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; + /// + /// 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 45a9bc98ed43..0c5d39f47a76 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 +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for request handler settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigRequestHandler)] +public class RequestHandlerSettings { - /// - /// 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 const bool StaticAddTrailingSlash = true; + internal const string StaticConvertUrlsToAscii = "try"; + internal const bool StaticEnableDefaultCharReplacements = true; - internal static readonly Umbraco.Cms.Core.Configuration.Models.CharItem[] DefaultCharCollection = - { + internal static readonly CharItem[] DefaultCharCollection = + { new () { Char = " ", Replacement = "-" }, new () { Char = "\"", Replacement = string.Empty }, new () { Char = "'", Replacement = string.Empty }, @@ -45,46 +42,46 @@ public class RequestHandlerSettings 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 cd82376c57d0..55fa7b2c5f3c 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"; + 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"; - private static readonly string[] s_default_plugins = new[] - { - "paste", - "anchor", - "charmap", - "table", - "lists", - "advlist", - "hr", - "autolink", - "directionality", - "tabfocus", - "searchreplace" - }; - private static readonly RichTextEditorCommand[] s_default_commands = new [] - { - 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}, - }; + internal const string StaticInvalidElements = "font"; + + private static readonly string[] Default_plugins = + { + "paste", "anchor", "charmap", "table", "lists", "advlist", "hr", "autolink", "directionality", "tabfocus", + "searchreplace", + }; - private static readonly IDictionary s_default_custom_config = new Dictionary() + private static readonly RichTextEditorCommand[] Default_commands = + { + 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 { - ["entity_encoding"] = "raw" - }; + 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, + }, + }; - /// - /// HTML RichText Editor TinyMCE Commands - /// - /// WB-TODO Custom Array of objects - public RichTextEditorCommand[] Commands { get; set; } = s_default_commands; + private static readonly IDictionary Default_custom_config = + new Dictionary { ["entity_encoding"] = "raw" }; - /// - /// HTML RichText Editor TinyMCE Plugins - /// - public string[] Plugins { get; set; } = s_default_plugins; + /// + /// HTML RichText Editor TinyMCE Commands + /// + /// WB-TODO Custom Array of objects + public RichTextEditorCommand[] Commands { get; set; } = Default_commands; - /// - /// HTML RichText Editor TinyMCE Custom Config - /// - /// WB-TODO Custom Dictionary - public IDictionary CustomConfig { get; set; } = s_default_custom_config; + /// + /// HTML RichText Editor TinyMCE Plugins + /// + public string[] Plugins { get; set; } = Default_plugins; - /// - /// - /// - [DefaultValue(StaticValidElements)] - public string ValidElements { get; set; } = StaticValidElements; + /// + /// HTML RichText Editor TinyMCE Custom Config + /// + /// WB-TODO Custom Dictionary + public IDictionary CustomConfig { get; set; } = Default_custom_config; - /// - /// Invalid HTML elements for RichText Editor - /// - [DefaultValue(StaticInvalidElements)] - public string InvalidElements { get; set; } = StaticInvalidElements; + /// + /// + [DefaultValue(StaticValidElements)] + public string ValidElements { get; set; } = StaticValidElements; - public class RichTextEditorCommand - { - [Required] - public string Alias { get; set; } = null!; + /// + /// 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 string Name { get; set; } = null!; - [Required] - public RichTextEditorCommandMode Mode { get; set; } - } + [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 db1e1526e583..37426fa84f6c 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 643e83bcac13..09c55c784b9d 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 ef67d4010264..ac4e51a1c26a 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 5ec94381b4c2..241b796d16fe 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 54b9ad6c84fe..5a9ec1b94fbb 100644 --- a/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs @@ -6,91 +6,95 @@ 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. + /// + 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, +} + +/// +/// 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 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 - } + public string? Username { get; set; } /// - /// Typed configuration options for SMTP settings. + /// Gets or sets a value for the SMTP password. /// - 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; } - } + public string? Password { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/TourSettings.cs b/src/Umbraco.Core/Configuration/Models/TourSettings.cs index cdc54dfe1fbe..aaf2063c64c1 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 +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for tour settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigTours)] +public class TourSettings { + internal const bool StaticEnableTours = true; + /// - /// Typed configuration options for tour settings. + /// Gets or sets a value indicating whether back-office tours are enabled. /// - [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; - } + [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 30ef3718f401..f281bbc31a36 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 d016e3547bfe..bec6d77bfb1b 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 08a4af566745..577fb9a2d9f7 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; + + /// + /// 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 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 an email for Unattended Installs + /// + [EmailAddress] + public string? UnattendedUserEmail { get; set; } = null; + /// - /// Typed configuration options for unattended settings. + /// Gets or sets a value to use for creating a user with a password for Unattended Installs /// - [UmbracoOptions(Constants.Configuration.ConfigUnattended)] - public class UnattendedSettings - { - private const bool StaticInstallUnattended = false; - private const bool StaticUpgradeUnattended = false; - - /// - /// 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 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 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; - } + 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 b53e98f712aa..156f90419c48 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; - - /// - [DefaultValue(StaticRequiredLength)] - public int RequiredLength { get; set; } = StaticRequiredLength; - - /// - [DefaultValue(StaticRequireNonLetterOrDigit)] - public bool RequireNonLetterOrDigit { get; set; } = StaticRequireNonLetterOrDigit; - - /// - [DefaultValue(StaticRequireDigit)] - public bool RequireDigit { get; set; } = StaticRequireDigit; - - /// - [DefaultValue(StaticRequireLowercase)] - public bool RequireLowercase { get; set; } = StaticRequireLowercase; - - /// - [DefaultValue(StaticRequireUppercase)] - public bool RequireUppercase { get; set; } = StaticRequireUppercase; - - /// - [DefaultValue(Constants.Security.AspNetCoreV3PasswordHashAlgorithmName)] - public string HashAlgorithmType { get; set; } = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName; - - /// - [DefaultValue(StaticMaxFailedAccessAttemptsBeforeLockout)] - public int MaxFailedAccessAttemptsBeforeLockout { get; set; } = StaticMaxFailedAccessAttemptsBeforeLockout; - } + 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(StaticRequireNonLetterOrDigit)] + public bool RequireNonLetterOrDigit { get; set; } = StaticRequireNonLetterOrDigit; + + /// + [DefaultValue(StaticRequireDigit)] + public bool RequireDigit { get; set; } = StaticRequireDigit; + + /// + [DefaultValue(StaticRequireLowercase)] + public bool RequireLowercase { get; set; } = StaticRequireLowercase; + + /// + [DefaultValue(StaticRequireUppercase)] + public bool RequireUppercase { get; set; } = StaticRequireUppercase; + + /// + [DefaultValue(Constants.Security.AspNetCoreV3PasswordHashAlgorithmName)] + public string HashAlgorithmType { get; set; } = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName; + + /// + [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 ca5d4d11e551..447a27f026d2 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 d21d6277bf4e..079801460038 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 31d0779626e3..32ad130c3376 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.Fail(message); + } - return ValidateOptionsResult.Success; + if (!ValidateSqlWriteLockTimeOutSetting(options.DistributedLockingWriteLockDefaultTimeout, out var message2)) + { + return ValidateOptionsResult.Fail(message2); } - 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; + 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 a8b63f39a0c6..ac0e1651eab9 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 - { - private readonly ICronTabParser _cronTabParser; + /// Helper for parsing crontab expressions. + public HealthChecksSettingsValidator(ICronTabParser cronTabParser) => _cronTabParser = 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) + /// + 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 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) + 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 6260341c181f..4a1872cf3063 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 3c073ac1008a..e262de76e7b3 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++; - } + setValues++; + } - if (!string.IsNullOrEmpty(options.UnattendedUserPassword)) - { - setValues++; - } + if (!string.IsNullOrEmpty(options.UnattendedUserEmail)) + { + 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."); - } + if (!string.IsNullOrEmpty(options.UnattendedUserPassword)) + { + setValues++; } - return ValidateOptionsResult.Success; + 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 970146a27eeb..ff858943ac49 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 deb7c64a9f3d..c4dff7a54281 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 - { - - 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"; + /// + /// 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 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 IIS custom errors should be skipped. - /// - [DefaultValue(StaticTrySkipIisCustomErrors)] - public bool TrySkipIisCustomErrors { get; set; } = StaticTrySkipIisCustomErrors; + /// + /// 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 an internal redirect should preserve the template. - /// - [DefaultValue(StaticInternalRedirectPreservesTemplate)] - public bool InternalRedirectPreservesTemplate { get; set; } = StaticInternalRedirectPreservesTemplate; + /// + /// 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 are disabled. - /// - [DefaultValue(StaticDisableAlternativeTemplates)] - public bool DisableAlternativeTemplates { get; set; } = StaticDisableAlternativeTemplates; + /// + /// 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 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 should be validated. + /// + [DefaultValue(StaticValidateAlternativeTemplates)] + public bool ValidateAlternativeTemplates { get; set; } = StaticValidateAlternativeTemplates; - /// - /// 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 find content ID by path is disabled. + /// + [DefaultValue(StaticDisableFindContentByIdPath)] + public bool DisableFindContentByIdPath { get; set; } = StaticDisableFindContentByIdPath; - /// - /// 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 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 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 1b1ebc6af5f5..bcd659c734a3 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 = null; + private static string? _modelsDirectoryAbsolute; - public static string ModelsDirectoryAbsolute(this ModelsBuilderSettings modelsBuilderConfig, IHostingEnvironment hostingEnvironment) + 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); - } + _modelsDirectoryAbsolute = GetModelsDirectory(root, modelsDirectory, modelsBuilderConfig.AcceptUnsafeModelsDirectory); + } + + return _modelsDirectoryAbsolute; + } - 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)) + { + throw new ConfigurationException($"Root is not rooted \"{root}\"."); } - // internal for tests - internal static string GetModelsDirectory(string root, string config, bool acceptUnsafe) + if (config.StartsWith("~/")) { - // making sure it is safe, ie under the website root, - // unless AcceptUnsafeModelsDirectory and then everything is OK. + var dir = Path.Combine(root, config.TrimStart("~/")); - if (!Path.IsPathRooted(root)) - throw new ConfigurationException($"Root is not rooted \"{root}\"."); + // 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 (config.StartsWith("~/")) + 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); + return dir; + } - throw new ConfigurationException($"Invalid models directory \"{config}\"."); + 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 064e035892fa..9e76710e2b58 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 f27d54b55d67..52256a29f071 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 506821df6d91..4c7472086081 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; } + RequiredLength = configSettings.RequiredLength; + RequireNonLetterOrDigit = configSettings.RequireNonLetterOrDigit; + RequireDigit = configSettings.RequireDigit; + RequireLowercase = configSettings.RequireLowercase; + RequireUppercase = configSettings.RequireUppercase; + HashAlgorithmType = configSettings.HashAlgorithmType; + MaxFailedAccessAttemptsBeforeLockout = configSettings.MaxFailedAccessAttemptsBeforeLockout; + } - public bool RequireNonLetterOrDigit { get; } + public int RequiredLength { get; } - public bool RequireDigit { get; } + public bool RequireNonLetterOrDigit { get; } - public bool RequireLowercase { get; } + public bool RequireDigit { get; } - public bool RequireUppercase { get; } + public bool RequireLowercase { get; } - public string HashAlgorithmType { get; } + public bool RequireUppercase { get; } - public int MaxFailedAccessAttemptsBeforeLockout { 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 b8049fe650ae..8740b81cb5b9 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; - } + return true; + } - if (y is null) - { - return false; - } + if (x is null) + { + return false; + } - if (x.GetType() != y.GetType()) - { - return false; - } + if (y is null) + { + return false; + } - return x.Char == y.Char && x.Replacement == y.Replacement; + if (x.GetType() != y.GetType()) + { + return false; } - public int GetHashCode(IChar obj) + return x.Char == y.Char && x.Replacement == y.Replacement; + } + + public int GetHashCode(IChar obj) + { + unchecked { - unchecked - { - return ((obj.Char != null ? obj.Char.GetHashCode() : 0) * 397) ^ (obj.Replacement != null ? obj.Replacement.GetHashCode() : 0); - } + 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 61e840245cf9..a2ba30b776bc 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 +namespace Umbraco.Cms.Core.Configuration.UmbracoSettings; + +public interface IChar { - public interface IChar - { - string Char { get; } + string Char { get; } - string Replacement { get; } - } + string Replacement { get; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IImagingAutoFillUploadField.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IImagingAutoFillUploadField.cs index c7d91a6d0af7..f6431dd77a0e 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 d79d8940c3fe..7a309d6fe34d 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 903f21f21ac2..1dfde6414f8b 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 4b4ad87801fd..9664d7cb7330 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 + Version = SemanticVersion.GetVersion(3); + } - /// - /// Gets the non-semantic version of the Umbraco code. - /// - public Version Version { get; } + /// + /// 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 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 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 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 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 6c30fbba713f..47b950de9cfe 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 da945731af26..dc36715585e6 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 - { - /// - /// alias for the content tree. - /// - public const string Content = "content"; + public const string Packages = "packages"; - /// - /// alias for the content blueprint tree. - /// - public const string ContentBlueprints = "contentBlueprints"; + /// + /// Application alias for the media section. + /// + public const string Media = "media"; - /// - /// alias for the member tree. - /// - public const string Members = "member"; + /// + /// Application alias for the members section. + /// + public const string Members = "member"; - /// - /// alias for the media tree. - /// - public const string Media = "media"; + /// + /// 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 macro tree. - /// - public const string Macros = "macros"; + /// + /// alias for the media tree. + /// + public const string Media = "media"; - /// - /// alias for the datatype tree. - /// - public const string DataTypes = "dataTypes"; + /// + /// alias for the macro tree. + /// + public const string Macros = "macros"; - /// - /// alias for the packages tree - /// - public const string Packages = "packages"; + /// + /// alias for the datatype tree. + /// + public const string DataTypes = "dataTypes"; - /// - /// alias for the dictionary tree. - /// - public const string Dictionary = "dictionary"; + /// + /// alias for the packages tree + /// + public const string Packages = "packages"; - public const string Stylesheets = "stylesheets"; + /// + /// alias for the dictionary tree. + /// + public const string Dictionary = "dictionary"; - /// - /// alias for the document type tree. - /// - public const string DocumentTypes = "documentTypes"; + public const string Stylesheets = "stylesheets"; - /// - /// alias for the media type tree. - /// - public const string MediaTypes = "mediaTypes"; + /// + /// alias for the document type tree. + /// + public const string DocumentTypes = "documentTypes"; - /// - /// alias for the member type tree. - /// - public const string MemberTypes = "memberTypes"; + /// + /// alias for the media type tree. + /// + public const string MediaTypes = "mediaTypes"; - /// - /// alias for the member group tree. - /// - public const string MemberGroups = "memberGroups"; + /// + /// alias for the member type tree. + /// + public const string MemberTypes = "memberTypes"; - /// - /// alias for the template tree. - /// - public const string Templates = "templates"; + /// + /// alias for the member group tree. + /// + public const string MemberGroups = "memberGroups"; - public const string RelationTypes = "relationTypes"; + /// + /// alias for the template tree. + /// + public const string Templates = "templates"; - public const string Languages = "languages"; + public const string RelationTypes = "relationTypes"; - /// - /// alias for the user types tree. - /// - public const string UserTypes = "userTypes"; + public const string Languages = "languages"; - /// - /// alias for the user permissions tree. - /// - public const string UserPermissions = "userPermissions"; + /// + /// alias for the user types tree. + /// + public const string UserTypes = "userTypes"; - /// - /// alias for the users tree. - /// - public const string Users = "users"; + /// + /// alias for the user permissions tree. + /// + public const string UserPermissions = "userPermissions"; - public const string Scripts = "scripts"; + /// + /// alias for the users tree. + /// + public const string Users = "users"; - public const string PartialViews = "partialViews"; + public const string Scripts = "scripts"; - public const string PartialViewMacros = "partialViewMacros"; + public const string PartialViews = "partialViews"; - public const string LogViewer = "logViewer"; + public const string PartialViewMacros = "partialViewMacros"; - public static class Groups - { - public const string Settings = "settingsGroup"; + public const string LogViewer = "logViewer"; - public const string Templating = "templatingGroup"; + public static class Groups + { + public const string Settings = "settingsGroup"; - public const string ThirdParty = "thirdPartyGroup"; - } + public const string Templating = "templatingGroup"; - // 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 54c51c95ffd9..f795a259749b 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 4be5ecba0443..832cac00e694 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 - /// - public static class CharArrays - { - /// - /// Char array containing only / - /// - public static readonly char[] ForwardSlash = new char[] { '/' }; - - /// - /// Char array containing only \ - /// - public static readonly char[] Backslash = new char[] { '\\' }; - - /// - /// Char array containing only ' - /// - public static readonly char[] SingleQuote = new char[] { '\'' }; - - /// - /// Char array containing only " - /// - public static readonly char[] DoubleQuote = new char[] { '\"' }; - - - /// - /// Char array containing ' " - /// - public static readonly char[] DoubleQuoteSingleQuote = new char[] { '\"', '\'' }; - - /// - /// Char array containing only _ - /// - public static readonly char[] Underscore = new char[] { '_' }; - - /// - /// Char array containing \n \r - /// - public static readonly char[] LineFeedCarriageReturn = new char[] { '\n', '\r' }; - - - /// - /// Char array containing \n - /// - public static readonly char[] LineFeed = new char[] { '\n' }; - - /// - /// Char array containing only , - /// - public static readonly char[] Comma = new char[] { ',' }; - - /// - /// Char array containing only & - /// - public static readonly char[] Ampersand = new char[] { '&' }; - - /// - /// Char array containing only \0 - /// - public static readonly char[] NullTerminator = new char[] { '\0' }; - - /// - /// Char array containing only . - /// - public static readonly char[] Period = new char[] { '.' }; - - /// - /// 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 = new char[] { '~', '/', '\\' }; - - /// - /// Char array containing only ? - /// - public static readonly char[] QuestionMark = new char[] { '?' }; - - /// - /// Char array containing ? & - /// - public static readonly char[] QuestionMarkAmpersand = new char[] { '?', '&' }; - - /// - /// Char array containing XML 1.1 whitespace chars - /// - public static readonly char[] XmlWhitespaceChars = new char[] { ' ', '\t', '\r', '\n' }; - - /// - /// Char array containing only the Space char - /// - public static readonly char[] Space = new char[] { ' ' }; - - /// - /// Char array containing only ; - /// - public static readonly char[] Semicolon = new char[] { ';' }; - - /// - /// Char array containing a comma and a space - /// - public static readonly char[] CommaSpace = new char[] { ',', ' ' }; - - /// - /// Char array containing _ - - /// - public static readonly char[] UnderscoreDash = new char[] { '_', '-' }; - - /// - /// 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 only / + /// + public static readonly char[] ForwardSlash = { '/' }; + + /// + /// Char array containing only \ + /// + public static readonly char[] Backslash = { '\\' }; + + /// + /// Char array containing only ' + /// + public static readonly char[] SingleQuote = { '\'' }; + + /// + /// Char array containing only " + /// + public static readonly char[] DoubleQuote = { '\"' }; + + /// + /// Char array containing ' " + /// + public static readonly char[] DoubleQuoteSingleQuote = { '\"', '\'' }; + + /// + /// Char array containing only _ + /// + public static readonly char[] Underscore = { '_' }; + + /// + /// Char array containing \n \r + /// + public static readonly char[] LineFeedCarriageReturn = { '\n', '\r' }; + + /// + /// Char array containing \n + /// + public static readonly char[] LineFeed = { '\n' }; + + /// + /// Char array containing only , + /// + public static readonly char[] Comma = { ',' }; + + /// + /// Char array containing only & + /// + public static readonly char[] Ampersand = { '&' }; + + /// + /// Char array containing only \0 + /// + public static readonly char[] NullTerminator = { '\0' }; + + /// + /// Char array containing only . + /// + public static readonly char[] Period = { '.' }; + + /// + /// Char array containing only ~ + /// + public static readonly char[] Tilde = { '~' }; + + /// + /// Char array containing ~ / + /// + public static readonly char[] TildeForwardSlash = { '~', '/' }; + + /// + /// Char array containing ~ / \ + /// + public static readonly char[] TildeForwardSlashBackSlash = { '~', '/', '\\' }; + + /// + /// Char array containing only ? + /// + public static readonly char[] QuestionMark = { '?' }; + + /// + /// Char array containing ? & + /// + public static readonly char[] QuestionMarkAmpersand = { '?', '&' }; + + /// + /// Char array containing XML 1.1 whitespace chars + /// + public static readonly char[] XmlWhitespaceChars = { ' ', '\t', '\r', '\n' }; + + /// + /// Char array containing only the Space char + /// + public static readonly char[] Space = { ' ' }; + + /// + /// Char array containing only ; + /// + public static readonly char[] Semicolon = { ';' }; + + /// + /// Char array containing a comma and a space + /// + public static readonly char[] CommaSpace = { ',', ' ' }; + + /// + /// Char array containing _ - + /// + public static readonly char[] UnderscoreDash = { '_', '-' }; + + /// + /// Char array containing = + /// + public static readonly char[] EqualsChar = { '=' }; + + /// + /// Char array containing > + /// + public static readonly char[] GreaterThan = { '>' }; + + /// + /// 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 747a74b8d8ba..e55c32d01a90 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 fd63ab385342..3f7f3188a991 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 cb34901e6c98..7b221e143578 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 { + 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 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 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"; + + /// + /// 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"; + } + + /// + /// Constants for Umbraco Media property aliases. + /// + 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; + } + + /// + /// Defines the alias identifiers for Umbraco media types. + /// + 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"; + } + + /// + /// 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 identifiers for property-type alias conventions that are used within the Umbraco core. + /// Defines the alias identifiers for Umbraco member types. /// - public static class Conventions + public static class MemberTypes { - 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 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 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"; - - /// - /// 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"; - } - - /// - /// Constants for Umbraco Media property aliases. - /// - 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; - } - - /// - /// Defines the alias identifiers for Umbraco media types. - /// - 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"; - } - - /// - /// Constants for Umbraco Member property aliases. - /// - 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"; - - /// - /// 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"; - } - - /// - /// 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; } = new[] { RelatedMediaAlias, RelatedDocumentAlias }; - - //TODO: return a list of built in types so we can use that to prevent deletion in the uI - } + /// + /// 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 ba8827cd26a9..a3e2dbc4c5dc 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 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 const string IgnoreUserStartNodes = "ignoreUserStartNodes"; + } - public static class ReservedPreValueKeys - { - public const string IgnoreUserStartNodes = "ignoreUserStartNodes"; - } - - /// - /// Defines the identifiers for Umbraco data types as constants for easy centralized access/management. + /// + /// 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 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); + public const string ContentPicker = "FD1E0DA5-5606-4862-B679-5D0CF3A52A59"; + /// + /// Guid for Member Picker as string + /// + public const string MemberPicker = "1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"; - /// - /// Guid for Member Picker as string - /// - 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 Member Picker - /// - public static readonly Guid MemberPickerGuid = new Guid(MemberPicker); + /// + /// 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 as string - /// - public const string MediaPicker = "135D60E0-64D9-49ED-AB08-893C9BA44AE5"; + /// + /// Guid for Media Picker v3 multiple as string + /// + public const string MediaPicker3Multiple = "1B661F40-2242-4B44-B9CB-3990EE2B13C0"; - /// - /// Guid for Media Picker - /// - public static readonly Guid MediaPickerGuid = new Guid(MediaPicker); + /// + /// 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 Multiple Media Picker as string - /// - public const string MultipleMediaPicker = "9DBBCBBB-2327-434A-B355-AF1B84E5010A"; + /// + /// Guid for Related Links as string + /// + public const string RelatedLinks = "B4E3535A-1753-47E2-8568-602CF8CFEE6F"; - /// - /// Guid for Multiple Media Picker - /// - public static readonly Guid MultipleMediaPickerGuid = new Guid(MultipleMediaPicker); + /// + /// 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 Media Picker v3 as string - /// - public const string MediaPicker3 = "4309A3EA-0D78-4329-A06C-C80B036AF19A"; + /// + /// Guid for Tags as string + /// + public const string Tags = "b6b73142-b9c1-4bf8-a16d-e1c23320b549"; - /// - /// Guid for Media Picker v3 - /// - public static readonly Guid MediaPicker3Guid = new Guid(MediaPicker3); + /// + /// Guid for List View - Content as string + /// + public const string ListViewContent = "C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"; - /// - /// Guid for Media Picker v3 multiple as string - /// - public const string MediaPicker3Multiple = "1B661F40-2242-4B44-B9CB-3990EE2B13C0"; + /// + /// Guid for List View - Media as string + /// + public const string ListViewMedia = "3A0156C4-3B8C-4803-BDC1-6871FAA83FFF"; - /// - /// Guid for Media Picker v3 multiple - /// - public static readonly Guid MediaPicker3MultipleGuid = new Guid(MediaPicker3Multiple); + /// + /// 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 Media Picker v3 single-image as string - /// - public const string MediaPicker3SingleImage = "AD9F0CF2-BDA2-45D5-9EA1-A63CFC873FD3"; + /// + /// Guid for Approved Color as string + /// + public const string ApprovedColor = "0225af17-b302-49cb-9176-b9f35cab9c17"; - /// - /// Guid for Media Picker v3 single-image - /// - public static readonly Guid MediaPicker3SingleImageGuid = new Guid(MediaPicker3SingleImage); + /// + /// 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 Media Picker v3 multi-image as string - /// - public const string MediaPicker3MultipleImages = "0E63D883-B62B-4799-88C3-157F82E83ECC"; + /// + /// Guid for Date Picker as string + /// + public const string DatePicker = "5046194e-4237-453c-a547-15db3a07c4e1"; - /// - /// Guid for Media Picker v3 multi-image - /// - public static readonly Guid MediaPicker3MultipleImagesGuid = new Guid(MediaPicker3MultipleImages); + /// + /// 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 Related Links as string - /// - public const string RelatedLinks = "B4E3535A-1753-47E2-8568-602CF8CFEE6F"; + /// + /// Guid for Checkbox as string + /// + public const string Checkbox = "92897bc6-a5f3-4ffe-ae27-f2e7e33dda49"; - /// - /// Guid for Related Links - /// - public static readonly Guid RelatedLinksGuid = new Guid(RelatedLinks); + /// + /// 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 Member as string - /// - public const string Member = "d59be02f-1df9-4228-aa1e-01917d806cda"; + /// + /// Guid for Textstring as string + /// + public const string Textstring = "0cc0eba1-9960-42c9-bf9b-60e150b429ae"; - /// - /// Guid for Member - /// - public static readonly Guid MemberGuid = new Guid(Member); + /// + /// 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 Image Cropper as string - /// - public const string ImageCropper = "1df9f033-e6d4-451f-b8d2-e0cbc50a836f"; + /// + /// Guid for UploadVideo as string + /// + public const string UploadVideo = "70575fe7-9812-4396-bbe1-c81a76db71b5"; - /// - /// Guid for Image Cropper - /// - public static readonly Guid ImageCropperGuid = new Guid(ImageCropper); + /// + /// 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 Tags as string - /// - public const string Tags = "b6b73142-b9c1-4bf8-a16d-e1c23320b549"; + /// + /// Guid for UploadVectorGraphics as string + /// + public const string UploadVectorGraphics = "215cb418-2153-4429-9aef-8c0f0041191b"; - /// - /// Guid for Tags - /// - public static readonly Guid TagsGuid = new Guid(Tags); + /// + /// 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 List View - Content as string - /// - public const string ListViewContent = "C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"; + /// + /// Guid for Label as big int + /// + public const string LabelBigInt = "930861bf-e262-4ead-a704-f99453565708"; - /// - /// Guid for List View - Content - /// - public static readonly Guid ListViewContentGuid = new Guid(ListViewContent); + /// + /// 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 List View - Media as string - /// - public const string ListViewMedia = "3A0156C4-3B8C-4803-BDC1-6871FAA83FFF"; + /// + /// Guid for Label as decimal + /// + public const string LabelDecimal = "8f1ef1e1-9de4-40d3-a072-6673f631ca64"; - /// - /// Guid for List View - Media - /// - public static readonly Guid ListViewMediaGuid = new Guid(ListViewMedia); + /// + /// Guid for Content Picker + /// + public static readonly Guid ContentPickerGuid = new(ContentPicker); + /// + /// Guid for Member Picker + /// + public static readonly Guid MemberPickerGuid = new(MemberPicker); - /// - /// Guid for List View - Members as string - /// - public const string ListViewMembers = "AA2C52A0-CE87-4E65-A47C-7DF09358585D"; + /// + /// Guid for Media Picker + /// + public static readonly Guid MediaPickerGuid = new(MediaPicker); - /// - /// Guid for List View - Members - /// - public static readonly Guid ListViewMembersGuid = new Guid(ListViewMembers); + /// + /// Guid for Multiple Media Picker + /// + public static readonly Guid MultipleMediaPickerGuid = new(MultipleMediaPicker); - /// - /// Guid for Date Picker with time as string - /// - public const string DatePickerWithTime = "e4d66c0f-b935-4200-81f0-025f7256b89a"; + /// + /// Guid for Media Picker v3 + /// + public static readonly Guid MediaPicker3Guid = new(MediaPicker3); - /// - /// Guid for Date Picker with time - /// - public static readonly Guid DatePickerWithTimeGuid = new Guid(DatePickerWithTime); + /// + /// 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 Approved Color as string - /// - public const string ApprovedColor = "0225af17-b302-49cb-9176-b9f35cab9c17"; + /// + /// Guid for Media Picker v3 multi-image + /// + public static readonly Guid MediaPicker3MultipleImagesGuid = new(MediaPicker3MultipleImages); - /// - /// Guid for Approved Color - /// - public static readonly Guid ApprovedColorGuid = new Guid(ApprovedColor); + /// + /// Guid for Related Links + /// + public static readonly Guid RelatedLinksGuid = new(RelatedLinks); + /// + /// Guid for Member + /// + public static readonly Guid MemberGuid = new(Member); - /// - /// Guid for Dropdown multiple as string - /// - public const string DropdownMultiple = "f38f0ac7-1d27-439c-9f3f-089cd8825a53"; + /// + /// Guid for Image Cropper + /// + public static readonly Guid ImageCropperGuid = new(ImageCropper); - /// - /// Guid for Dropdown multiple - /// - public static readonly Guid DropdownMultipleGuid = new Guid(DropdownMultiple); + /// + /// Guid for Tags + /// + public static readonly Guid TagsGuid = new(Tags); + /// + /// Guid for List View - Content + /// + public static readonly Guid ListViewContentGuid = new(ListViewContent); - /// - /// Guid for Radiobox as string - /// - public const string Radiobox = "bb5f57c9-ce2b-4bb9-b697-4caca783a805"; + /// + /// Guid for List View - Media + /// + public static readonly Guid ListViewMediaGuid = new(ListViewMedia); - /// - /// Guid for Radiobox - /// - public static readonly Guid RadioboxGuid = new Guid(Radiobox); + /// + /// 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 Date Picker as string - /// - public const string DatePicker = "5046194e-4237-453c-a547-15db3a07c4e1"; + /// + /// Guid for Approved Color + /// + public static readonly Guid ApprovedColorGuid = new(ApprovedColor); - /// - /// Guid for Date Picker - /// - public static readonly Guid DatePickerGuid = new Guid(DatePicker); + /// + /// Guid for Dropdown multiple + /// + public static readonly Guid DropdownMultipleGuid = new(DropdownMultiple); + /// + /// Guid for Radiobox + /// + public static readonly Guid RadioboxGuid = new(Radiobox); - /// - /// Guid for Dropdown as string - /// - public const string Dropdown = "0b6a45e7-44ba-430d-9da5-4e46060b9e03"; + /// + /// Guid for Date Picker + /// + public static readonly Guid DatePickerGuid = new(DatePicker); - /// - /// Guid for Dropdown - /// - public static readonly Guid DropdownGuid = new Guid(Dropdown); + /// + /// Guid for Dropdown + /// + public static readonly Guid DropdownGuid = new(Dropdown); + /// + /// Guid for Checkbox list + /// + public static readonly Guid CheckboxListGuid = new(CheckboxList); - /// - /// Guid for Checkbox list as string - /// - public const string CheckboxList = "fbaf13a8-4036-41f2-93a3-974f678c312a"; + /// + /// Guid for Checkbox + /// + public static readonly Guid CheckboxGuid = new(Checkbox); - /// - /// Guid for Checkbox list - /// - public static readonly Guid CheckboxListGuid = new Guid(CheckboxList); + /// + /// Guid for Dropdown + /// + public static readonly Guid NumericGuid = new(Numeric); + /// + /// Guid for Richtext editor + /// + public static readonly Guid RichtextEditorGuid = new(RichtextEditor); - /// - /// Guid for Checkbox as string - /// - public const string Checkbox = "92897bc6-a5f3-4ffe-ae27-f2e7e33dda49"; + /// + /// Guid for Textstring + /// + public static readonly Guid TextstringGuid = new(Textstring); - /// - /// Guid for Checkbox - /// - public static readonly Guid CheckboxGuid = new Guid(Checkbox); + /// + /// Guid for Dropdown + /// + public static readonly Guid TextareaGuid = new(Textarea); + /// + /// Guid for Upload + /// + public static readonly Guid UploadGuid = new(Upload); - /// - /// Guid for Numeric as string - /// - public const string Numeric = "2e6d3631-066e-44b8-aec4-96f09099b2b5"; + /// + /// Guid for UploadVideo + /// + public static readonly Guid UploadVideoGuid = new(UploadVideo); - /// - /// Guid for Dropdown - /// - public static readonly Guid NumericGuid = new Guid(Numeric); + /// + /// Guid for UploadAudio + /// + public static readonly Guid UploadAudioGuid = new(UploadAudio); + /// + /// Guid for UploadArticle + /// + public static readonly Guid UploadArticleGuid = new(UploadArticle); - /// - /// Guid for Richtext editor as string - /// - public const string RichtextEditor = "ca90c950-0aff-4e72-b976-a30b1ac57dad"; + /// + /// Guid for UploadVectorGraphics + /// + public static readonly Guid UploadVectorGraphicsGuid = new(UploadVectorGraphics); - /// - /// Guid for Richtext editor - /// - public static readonly Guid RichtextEditorGuid = new Guid(RichtextEditor); + /// + /// Guid for Label string + /// + public static readonly Guid LabelStringGuid = new(LabelString); + /// + /// Guid for Label int + /// + public static readonly Guid LabelIntGuid = new(LabelInt); - /// - /// Guid for Textstring as string - /// - public const string Textstring = "0cc0eba1-9960-42c9-bf9b-60e150b429ae"; + /// + /// Guid for Label big int + /// + public static readonly Guid LabelBigIntGuid = new(LabelBigInt); - /// - /// Guid for Textstring - /// - public static readonly Guid TextstringGuid = new Guid(Textstring); + /// + /// 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 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); - - - } + /// + /// 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 30daacf42b66..0f552e8a8290 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 5a8ea401cbdf..2980a5945719 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 CompilationDebugCheck = "https://umbra.co/healthchecks-compilation-debug"; - } + public const string TrySkipIisCustomErrorsCheck = + "https://umbra.co/healthchecks-skip-iis-custom-errors"; - 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 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 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 Security + public static class HttpsCheck { - 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 474ec49a5085..677f4420856d 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 7be1fbd1406c..a89bfc2553fc 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 39980f116a80..40ab52aaa5ab 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 blueprint icon - /// - public const string Blueprint = "icon-blueprint"; - - /// - /// System content icon - /// - public const string Content = "icon-document"; - - /// - /// System content type icon - /// - public const string ContentType = "icon-item-arrangement"; - - /// - /// System data type icon - /// - public const string DataType = "icon-autofill"; - - /// - /// System dictionary icon - /// - public const string Dictionary = "icon-book-alt"; - - /// - /// System generic folder icon - /// - public const string Folder = "icon-folder"; - - /// - /// System language icon - /// - public const string Language = "icon-globe"; - - /// - /// System logviewer icon - /// - public const string LogViewer = "icon-box-alt"; - - /// - /// System list view icon - /// - public const string ListView = "icon-thumbnail-list"; - - /// - /// System macro icon - /// - public const string Macro = "icon-settings-alt"; - - /// - /// System media file icon - /// - public const string MediaFile = "icon-document"; - - /// - /// System media video icon - /// - public const string MediaVideo = "icon-video"; - - /// - /// System media audio icon - /// - public const string MediaAudio = "icon-sound-waves"; - - /// - /// System media article icon - /// - public const string MediaArticle = "icon-article"; - - /// - /// System media vector icon - /// - public const string MediaVectorGraphics = "icon-picture"; - - /// - /// System media folder icon - /// - public const string MediaFolder = "icon-folder"; - - /// - /// System media image icon - /// - public const string MediaImage = "icon-picture"; - - /// - /// System media type icon - /// - public const string MediaType = "icon-thumbnails"; - - /// - /// System member icon - /// - public const string Member = "icon-user"; - - /// - /// System member group icon - /// - public const string MemberGroup = "icon-users-alt"; - - /// - /// System member type icon - /// - public const string MemberType = "icon-users"; - - /// - /// System packages icon - /// - public const string Packages = "icon-box"; - - /// - /// System property editor icon - /// - public const string PropertyEditor = "icon-autofill"; - - /// - /// System member icon - /// - public const string Template = "icon-layout"; - - /// - /// System user icon - /// - public const string User = "icon-user"; - - /// - /// System user group icon - /// - public const string UserGroup = "icon-users"; - } + /// + /// System default icon + /// + public const string DefaultIcon = Content; + + /// + /// System blueprint icon + /// + public const string Blueprint = "icon-blueprint"; + + /// + /// System content icon + /// + public const string Content = "icon-document"; + + /// + /// System content type icon + /// + public const string ContentType = "icon-item-arrangement"; + + /// + /// System data type icon + /// + public const string DataType = "icon-autofill"; + + /// + /// System dictionary icon + /// + public const string Dictionary = "icon-book-alt"; + + /// + /// System generic folder icon + /// + public const string Folder = "icon-folder"; + + /// + /// System language icon + /// + public const string Language = "icon-globe"; + + /// + /// System logviewer icon + /// + public const string LogViewer = "icon-box-alt"; + + /// + /// System list view icon + /// + public const string ListView = "icon-thumbnail-list"; + + /// + /// System macro icon + /// + public const string Macro = "icon-settings-alt"; + + /// + /// System media file icon + /// + public const string MediaFile = "icon-document"; + + /// + /// System media video icon + /// + public const string MediaVideo = "icon-video"; + + /// + /// System media audio icon + /// + public const string MediaAudio = "icon-sound-waves"; + + /// + /// System media article icon + /// + public const string MediaArticle = "icon-article"; + + /// + /// System media vector icon + /// + public const string MediaVectorGraphics = "icon-picture"; + + /// + /// System media folder icon + /// + public const string MediaFolder = "icon-folder"; + + /// + /// System media image icon + /// + public const string MediaImage = "icon-picture"; + + /// + /// System media type icon + /// + public const string MediaType = "icon-thumbnails"; + + /// + /// System member icon + /// + public const string Member = "icon-user"; + + /// + /// System member group icon + /// + public const string MemberGroup = "icon-users-alt"; + + /// + /// System member type icon + /// + public const string MemberType = "icon-users"; + + /// + /// System packages icon + /// + public const string Packages = "icon-box"; + + /// + /// System property editor icon + /// + public const string PropertyEditor = "icon-autofill"; + + /// + /// System member icon + /// + public const string Template = "icon-layout"; + + /// + /// System user icon + /// + public const string User = "icon-user"; + + /// + /// 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 fcf2e7ed1475..9c5d9ca48e67 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 289c0355a8ab..63b852a6002b 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 0a9847b8488b..049a536690ff 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 { - /// - /// Defines the Umbraco object type unique identifiers. - /// - public static class ObjectTypes - { - /// - /// 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 - - public const string DataTypeContainer = "521231E3-8B37-469C-9F9D-51AFC91FEB7B"; + public static readonly Guid SystemRoot = new(Strings.SystemRoot); - public const string DocumentTypeContainer = "2F7A2769-6B0B-4468-90DD-AF42D64F7F16"; + public static readonly Guid ContentRecycleBin = new(Strings.ContentRecycleBin); - public const string MediaTypeContainer = "42AEF799-B288-4744-9B10-BE144B73CDC4"; + public static readonly Guid MediaRecycleBin = new(Strings.MediaRecycleBin); - public const string ContentItem = "10E2B09F-C28B-476D-B77A-AA686435E44A"; + public static readonly Guid DataTypeContainer = new(Strings.DataTypeContainer); - public const string ContentItemType = "7A333C54-6F43-40A4-86A2-18688DC7E532"; + public static readonly Guid DocumentTypeContainer = new(Strings.DocumentTypeContainer); - public const string ContentRecycleBin = "01BB7FF2-24DC-4C0C-95A2-C24EF72BBAC8"; + public static readonly Guid MediaTypeContainer = new(Strings.MediaTypeContainer); - public const string DataType = "30A2A501-1978-4DDB-A57B-F7EFED43BA3C"; + public static readonly Guid DataType = new(Strings.DataType); - public const string Document = "C66BA18E-EAF3-4CFF-8A22-41B16D66A972"; + public static readonly Guid Document = new(Strings.Document); - public const string DocumentBlueprint = "6EBEF410-03AA-48CF-A792-E1C1CB087ACA"; + public static readonly Guid DocumentBlueprint = new(Strings.DocumentBlueprint); - public const string DocumentType = "A2CB7800-F571-4787-9638-BC48539A0EFB"; + public static readonly Guid DocumentType = new(Strings.DocumentType); - public const string Media = "B796F64C-1F99-4FFB-B886-4BF4BC011A9C"; + public static readonly Guid Media = new(Strings.Media); - public const string MediaRecycleBin = "CF3D8E34-1C1C-41e9-AE56-878B57B32113"; + public static readonly Guid MediaType = new(Strings.MediaType); - public const string MediaType = "4EA4382B-2F5A-4C2B-9587-AE9B3CF3602E"; + public static readonly Guid Member = new(Strings.Member); - public const string Member = "39EB0F98-B348-42A1-8662-E7EB18487560"; + public static readonly Guid MemberGroup = new(Strings.MemberGroup); - public const string MemberGroup = "366E63B9-880F-4E13-A61C-98069B029728"; + public static readonly Guid MemberType = new(Strings.MemberType); - public const string MemberType = "9B5416FB-E72F-45A9-A07B-5A9A2709CE43"; + public static readonly Guid TemplateType = new(Strings.Template); - public const string SystemRoot = "EA7D8624-4CFE-4578-A871-24AA946BF34D"; + public static readonly Guid LockObject = new(Strings.LockObject); - public const string Template = "6FBDE604-4178-42CE-A10B-8A2600A2F07D"; + public static readonly Guid RelationType = new(Strings.RelationType); - public const string LockObject = "87A9F1FF-B1E4-4A25-BABB-465A4A47EC41"; + public static readonly Guid FormsForm = new(Strings.FormsForm); - public const string RelationType = "B1988FAD-8675-4F47-915A-B3A602BC5D8D"; + public static readonly Guid FormsPreValue = new(Strings.FormsPreValue); - public const string FormsForm = "F5A9F787-6593-46F0-B8FF-BFD9BCA9F6BB"; + public static readonly Guid FormsDataSource = new(Strings.FormsDataSource); - public const string FormsPreValue = "42D7BF9B-A362-4FEE-B45A-674D5C064B70"; + public static readonly Guid Language = new(Strings.Language); - public const string FormsDataSource = "CFED6CE4-9359-443E-9977-9956FEB1D867"; + public static readonly Guid IdReservation = new(Strings.IdReservation); - public const string Language = "6B05D05B-EC78-49BE-A4E4-79E274F07A77"; + public static readonly Guid Template = new(Strings.Template); - public const string IdReservation = "92849B1E-3904-4713-9356-F646F87C25F4"; + public static readonly Guid ContentItem = new(Strings.ContentItem); - // ReSharper restore MemberHidesStaticFromOuterClass - } + /// + /// 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 + public const string DataTypeContainer = "521231E3-8B37-469C-9F9D-51AFC91FEB7B"; - public static readonly Guid SystemRoot = new Guid(Strings.SystemRoot); + public const string DocumentTypeContainer = "2F7A2769-6B0B-4468-90DD-AF42D64F7F16"; - public static readonly Guid ContentRecycleBin = new Guid(Strings.ContentRecycleBin); + public const string MediaTypeContainer = "42AEF799-B288-4744-9B10-BE144B73CDC4"; - public static readonly Guid MediaRecycleBin = new Guid(Strings.MediaRecycleBin); + public const string ContentItem = "10E2B09F-C28B-476D-B77A-AA686435E44A"; - public static readonly Guid DataTypeContainer = new Guid(Strings.DataTypeContainer); + public const string ContentItemType = "7A333C54-6F43-40A4-86A2-18688DC7E532"; - public static readonly Guid DocumentTypeContainer = new Guid(Strings.DocumentTypeContainer); + public const string ContentRecycleBin = "01BB7FF2-24DC-4C0C-95A2-C24EF72BBAC8"; - public static readonly Guid MediaTypeContainer = new Guid(Strings.MediaTypeContainer); + public const string DataType = "30A2A501-1978-4DDB-A57B-F7EFED43BA3C"; - public static readonly Guid DataType = new Guid(Strings.DataType); + public const string Document = "C66BA18E-EAF3-4CFF-8A22-41B16D66A972"; - public static readonly Guid Document = new Guid(Strings.Document); + public const string DocumentBlueprint = "6EBEF410-03AA-48CF-A792-E1C1CB087ACA"; - public static readonly Guid DocumentBlueprint = new Guid(Strings.DocumentBlueprint); + public const string DocumentType = "A2CB7800-F571-4787-9638-BC48539A0EFB"; - public static readonly Guid DocumentType = new Guid(Strings.DocumentType); + public const string Media = "B796F64C-1F99-4FFB-B886-4BF4BC011A9C"; - public static readonly Guid Media = new Guid(Strings.Media); + public const string MediaRecycleBin = "CF3D8E34-1C1C-41e9-AE56-878B57B32113"; - public static readonly Guid MediaType = new Guid(Strings.MediaType); + public const string MediaType = "4EA4382B-2F5A-4C2B-9587-AE9B3CF3602E"; - public static readonly Guid Member = new Guid(Strings.Member); + public const string Member = "39EB0F98-B348-42A1-8662-E7EB18487560"; - public static readonly Guid MemberGroup = new Guid(Strings.MemberGroup); + public const string MemberGroup = "366E63B9-880F-4E13-A61C-98069B029728"; - public static readonly Guid MemberType = new Guid(Strings.MemberType); + public const string MemberType = "9B5416FB-E72F-45A9-A07B-5A9A2709CE43"; - public static readonly Guid TemplateType = new Guid(Strings.Template); + public const string SystemRoot = "EA7D8624-4CFE-4578-A871-24AA946BF34D"; - public static readonly Guid LockObject = new Guid(Strings.LockObject); + public const string Template = "6FBDE604-4178-42CE-A10B-8A2600A2F07D"; - public static readonly Guid RelationType = new Guid(Strings.RelationType); + public const string LockObject = "87A9F1FF-B1E4-4A25-BABB-465A4A47EC41"; - public static readonly Guid FormsForm = new Guid(Strings.FormsForm); + public const string RelationType = "B1988FAD-8675-4F47-915A-B3A602BC5D8D"; - public static readonly Guid FormsPreValue = new Guid(Strings.FormsPreValue); + public const string FormsForm = "F5A9F787-6593-46F0-B8FF-BFD9BCA9F6BB"; - public static readonly Guid FormsDataSource = new Guid(Strings.FormsDataSource); + public const string FormsPreValue = "42D7BF9B-A362-4FEE-B45A-674D5C064B70"; - public static readonly Guid Language = new Guid(Strings.Language); + public const string FormsDataSource = "CFED6CE4-9359-443E-9977-9956FEB1D867"; - public static readonly Guid IdReservation = new Guid(Strings.IdReservation); + public const string Language = "6B05D05B-EC78-49BE-A4E4-79E274F07A77"; - public static readonly Guid Template = new Guid(Strings.Template); + public const string IdReservation = "92849B1E-3904-4713-9356-F646F87C25F4"; - 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 96ef39b7c136..96746adb4940 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 b34351d902f9..2bb53b32994b 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 + { + 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 { /// - /// Used to prefix generic properties that are internal content properties + /// Block List. /// - public const string InternalGenericPropertiesPrefix = "_umb_"; + public const string BlockList = "Umbraco.BlockList"; - 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"; - - } - } + /// + /// CheckBox List. + /// + public const string CheckBoxList = "Umbraco.CheckBoxList"; /// - /// Defines Umbraco built-in property editor aliases. + /// Color Picker. /// - 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 ColorPicker = "Umbraco.ColorPicker"; /// - /// Defines Umbraco build-in datatype configuration keys. + /// Eye Dropper Color Picker. /// - 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 ColorPickerEyeDropper = "Umbraco.ColorPicker.EyeDropper"; /// - /// Defines Umbraco's built-in property editor groups. + /// Content Picker. /// - public static class Groups - { - public const string Common = "Common"; + public const string ContentPicker = "Umbraco.ContentPicker"; - public const string Lists = "Lists"; + /// + /// DateTime. + /// + public const string DateTime = "Umbraco.DateTime"; - public const string Media = "Media"; + /// + /// DropDown List. + /// + public const string DropDownListFlexible = "Umbraco.DropDown.Flexible"; - public const string People = "People"; + /// + /// Grid. + /// + public const string Grid = "Umbraco.Grid"; - public const string Pickers = "Pickers"; + /// + /// Image Cropper. + /// + public const string ImageCropper = "Umbraco.ImageCropper"; - public const string RichContent = "Rich Content"; - } + /// + /// 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 46b41ea23368..a713b279b138 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 68601a78b0ab..26e26804ae50 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; - - 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 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 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 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."; - 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"; - - /// - /// 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\"}"; - } + /// + /// Gets the identifier of the 'super' user. + /// + public const int SuperUserId = -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 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 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 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."; + + 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"; + + /// + /// 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 b57861c92ac8..f89368046564 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 a2fe501ab339..549dae5bd62f 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 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 static class DataTypeRepository + { + public const string EnsureUniqueNodeName = "Umbraco.Core.DataTypeDefinitionRepository.EnsureUniqueNodeName"; + } + + public static class NuCacheDatabaseDataSource { - 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 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"; - - } + 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 0ad985267195..43de01995bde 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; - - /// - /// The string identifier for global system root node. - /// - /// Use this instead of re-creating the string everywhere. - public const string RootString = "-1"; + public const int Root = -1; - /// - /// The integer identifier for content's recycle bin. - /// - public const int RecycleBinContent = -20; + /// + /// 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 content's recycle bin. - /// - /// Use this instead of re-creating the string everywhere. - public const string RecycleBinContentString = "-20"; + /// + /// The integer identifier for content's recycle bin. + /// + public const int RecycleBinContent = -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 identifier for content's recycle bin. + /// + /// Use this instead of re-creating the string everywhere. + public const string RecycleBinContentString = "-20"; - /// - /// The integer identifier for media's recycle bin. - /// - public const int RecycleBinMedia = -21; + /// + /// 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 identifier for media's recycle bin. - /// - /// Use this instead of re-creating the string everywhere. - public const string RecycleBinMediaString = "-21"; + /// + /// The integer identifier for media's recycle bin. + /// + public const int RecycleBinMedia = -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 identifier for media's recycle bin. + /// + /// Use this instead of re-creating the string everywhere. + public const string RecycleBinMediaString = "-21"; - public const int DefaultLabelDataTypeId = -92; + /// + /// 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 string UmbracoDefaultDatabaseName = "Umbraco"; + public const int DefaultLabelDataTypeId = -92; - public const string UmbracoConnectionName = "umbracoDbDSN"; + public const string UmbracoDefaultDatabaseName = "Umbraco"; - 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 f70dd199fc02..85375390ac0a 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 6fc474d9aeb3..5f3783a77436 100644 --- a/src/Umbraco.Core/Constants-Telemetry.cs +++ b/src/Umbraco.Core/Constants-Telemetry.cs @@ -1,32 +1,30 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class Telemetry { - 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"; - } + 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 01e9ca213d22..f65c29051614 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 - - - public const string Unknown = "unknown"; - - // guid entity types - - public const string AnyGuid = "any-guid"; // that one is for tests - - public const string Element = "element"; - public const string Document = "document"; - - public const string DocumentBlueprint = "document-blueprint"; - - public const string Media = "media"; - public const string Member = "member"; - - public const string DictionaryItem = "dictionary-item"; - public const string Macro = "macro"; - public const string Template = "template"; - - public const string DocumentType = "document-type"; - public const string DocumentTypeContainer = "document-type-container"; - - // 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 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"; - - - - - } + // 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 Element = "element"; + public const string Document = "document"; + + public const string DocumentBlueprint = "document-blueprint"; + + public const string Media = "media"; + public const string Member = "member"; + + public const string DictionaryItem = "dictionary-item"; + public const string Macro = "macro"; + public const string Template = "template"; + + public const string DocumentType = "document-type"; + public const string DocumentTypeContainer = "document-type-container"; + + // 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 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"; } } diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index f6a8c00970ab..bfbe4e56d52e 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 - { - /// - /// The preview cookie name - /// - public const string PreviewCookieName = "UMB_PREVIEW"; + 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"; + /// + /// 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"; + 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 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 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 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 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"; - /// - /// The default authentication type used for remembering that 2FA is not needed on next login - /// - public const string TwoFactorRememberBrowserCookie = "TwoFactorRememberBrowser"; + public const string + BackOfficePathSegment = "BackOffice"; // The path segment prefix for all back office controllers - 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 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 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 e4a5eedf1868..09a3e410fde5 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.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) - { - _logger = logger; - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - } + public IEnumerable GetContentAppsFor(object o, IEnumerable? userGroups = null) + { + IEnumerable roles = GetCurrentUserGroups(); - private IEnumerable GetCurrentUserGroups() - { - var currentUser = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser; - return currentUser == null - ? Enumerable.Empty() - : currentUser.Groups; + var apps = this.Select(x => x.GetContentAppFor(o, roles)).WhereNotNull().OrderBy(x => x.Weight).ToList(); - } + var aliases = new HashSet(); + List? dups = null; - public IEnumerable GetContentAppsFor(object o, IEnumerable? userGroups = null) + foreach (ContentApp app in apps) { - 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)); - } + if (dups != null) + { + // dying is not user-friendly, so let's write to log instead, and wish people read logs... - return apps; + // 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 a80c79a3ef55..fe6fdd423a2b 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.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; + protected override ContentAppFactoryCollectionBuilder This => this; - // 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); - } + // 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 + ILoggerFactory loggerFactory = factory.GetRequiredService(); + IBackOfficeSecurityAccessor 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 948c563ea996..ac8b3a206107 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; + // see note on ContentApp + internal const int Weight = -100; - private ContentApp? _contentApp; - private ContentApp? _mediaApp; - private ContentApp? _memberApp; + private ContentApp? _contentApp; + private ContentApp? _mediaApp; + private ContentApp? _memberApp; - public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) + 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 3e068750c4b5..1e318e380ee6 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; + // see note on ContentApp + private const int Weight = +100; - private ContentApp? _contentApp; - private ContentApp? _mediaApp; - private ContentApp? _memberApp; + private ContentApp? _contentApp; + private ContentApp? _mediaApp; + private ContentApp? _memberApp; - public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) + 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 0fe482e7d4e5..5e4f6a7a888a 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 const int Weight = -200; - private ContentApp? _contentTypeApp; + private ContentApp? _contentTypeApp; - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + 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 6ddf98e132fb..8aed04050f6f 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 const int Weight = -180; - private ContentApp? _contentTypeApp; + private ContentApp? _contentTypeApp; - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + 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 98b82d24e77a..b585a7db4d08 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 const int Weight = -160; - private ContentApp? _contentTypeApp; + private ContentApp? _contentTypeApp; - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + 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 74e57d76c92b..712e1e7c1e0f 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 const int Weight = -140; - private ContentApp? _contentTypeApp; + private ContentApp? _contentTypeApp; - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + 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 ae8a957df7e1..21bfcfcef024 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 const int Weight = -100; - private ContentApp? _dictionaryApp; + private ContentApp? _dictionaryApp; - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + 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 d33c50499f53..466c9d7a3baf 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.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 static ContentApp CreateContentApp( + IDataTypeService dataTypeService, + PropertyEditorCollection propertyEditors, + string entityType, + string contentTypeAlias, + int defaultListViewDataType) + { + if (dataTypeService == null) + { + throw new ArgumentNullException(nameof(dataTypeService)); + } - public ListViewContentAppFactory(IDataTypeService dataTypeService, PropertyEditorCollection propertyEditors) + if (propertyEditors == null) { - _dataTypeService = dataTypeService; - _propertyEditors = propertyEditors; + throw new ArgumentNullException(nameof(propertyEditors)); } - public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) + if (string.IsNullOrWhiteSpace(entityType)) { - string contentTypeAlias, entityType; - int dtdId; + throw new ArgumentException("message", nameof(entityType)); + } - 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; - } + if (string.IsNullOrWhiteSpace(contentTypeAlias)) + { + throw new ArgumentException("message", nameof(contentTypeAlias)); + } - return CreateContentApp(_dataTypeService, _propertyEditors, entityType, contentTypeAlias, dtdId); + if (defaultListViewDataType == default) + { + throw new ArgumentException("defaultListViewDataType", nameof(defaultListViewDataType)); } - public static ContentApp CreateContentApp(IDataTypeService dataTypeService, - PropertyEditorCollection propertyEditors, - string entityType, string contentTypeAlias, - int defaultListViewDataType) + var contentApp = new ContentApp { - 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)); + Alias = "umbListView", + Name = "Child items", + Icon = "icon-list", + View = "views/content/apps/listview/listview.html", + Weight = Weight, + }; - 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; - 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); - //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"); + } - 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"); + } - var 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); - var listViewConfig = editor.GetConfigurationEditor().ToConfigurationEditor(dt.Configuration); - //add the entity type to the config - listViewConfig["entityType"] = entityType; + // add the entity type to the config + listViewConfig["entityType"] = entityType; - //Override Tab Label if tabName is provided - if (listViewConfig.ContainsKey("tabName")) + // Override Tab Label if tabName is provided + if (listViewConfig.ContainsKey("tabName")) + { + var configTabName = listViewConfig["tabName"]; + if (string.IsNullOrWhiteSpace(configTabName.ToString()) == false) { - var configTabName = listViewConfig["tabName"]; - if (configTabName != null && String.IsNullOrWhiteSpace(configTabName.ToString()) == false) - contentApp.Name = configTabName.ToString(); + contentApp.Name = configTabName.ToString(); } + } - //Override Icon if icon is provided - if (listViewConfig.ContainsKey("icon")) + // Override Icon if icon is provided + if (listViewConfig.ContainsKey("icon")) + { + var configIcon = listViewConfig["icon"]; + if (string.IsNullOrWhiteSpace(configIcon.ToString()) == false) { - var configIcon = listViewConfig["icon"]; - if (configIcon != null && String.IsNullOrWhiteSpace(configIcon.ToString()) == false) - contentApp.Icon = configIcon.ToString(); + 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; - } + // 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 + // This is the view model used for the list view app + contentApp.ViewModel = new List + { + new() { - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}containerView", - Label = "", - Value = null, - View = editor.GetValueEditor().View, - HideLabel = true, - Config = listViewConfig - } - }; - - return contentApp; + 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 ae5e783bbcad..5ba19cabb042 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; + // see note on ContentApp + internal const int Weight = +50; - private ContentApp? _memberApp; + private ContentApp? _memberApp; - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + 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 2f9203ef92a7..7d79338142d3 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 253f070b4061..bacfec7ef9aa 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) + { + if (sourceType == typeof(string)) + { + return true; + } + + return base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + if (value is string str) { - if (sourceType == typeof(string)) + 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 070659518e32..eb7383f60169 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 103d944de854..63d92fc38ac8 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 1be6e045d014..07688832f66a 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 135fe4304d79..ff3a0031b37e 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 e5c8378139f2..ebcf79fc7f1b 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 348e81e38379..50867c90f4a0 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) - { - // 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(); + IEnumerable dashboardSections = + Merge(base.CreateItems(factory), manifestParser.CombinedManifest.Dashboards); - var dashboardSections = Merge(base.CreateItems(factory), manifestParser.CombinedManifest.Dashboards); + return dashboardSections; + } - return dashboardSections; - } + private IEnumerable Merge( + IEnumerable dashboardsFromCode, + IReadOnlyList dashboardFromManifest) => + dashboardsFromCode.Concat(dashboardFromManifest) + .Where(x => !string.IsNullOrEmpty(x.Alias)) + .OrderBy(GetWeight); - private IEnumerable Merge(IEnumerable dashboardsFromCode, IReadOnlyList dashboardFromManifest) + private int GetWeight(IDashboard dashboard) + { + switch (dashboard) { - return dashboardsFromCode.Concat(dashboardFromManifest) - .Where(x => !string.IsNullOrEmpty(x.Alias)) - .OrderBy(GetWeight); - } + case ManifestDashboard manifestDashboardDefinition: + return manifestDashboardDefinition.Weight; - 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 9ff2b51bafa6..a79392c0d05d 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 5411f1d3cea1..ddd048c99e04 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 -{ - [Weight(20)] - public class ExamineDashboard : IDashboard - { - public string Alias => "settingsExamine"; - - public string[] Sections => new [] { "settings" }; +namespace Umbraco.Cms.Core.Dashboards; - public string View => "views/dashboard/settings/examinemanagement.html"; +[Weight(20)] +public class ExamineDashboard : IDashboard +{ + public string Alias => "settingsExamine"; - 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 c56ad7c51a0e..414655348468 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 24b4efaf6dbb..85c20534502e 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 -{ - [Weight(50)] - public class HealthCheckDashboard : IDashboard - { - public string Alias => "settingsHealthCheck"; - - public string[] Sections => new [] { "settings" }; +namespace Umbraco.Cms.Core.Dashboards; - public string View => "views/dashboard/settings/healthcheck.html"; +[Weight(50)] +public class HealthCheckDashboard : IDashboard +{ + public string Alias => "settingsHealthCheck"; - 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 9f8c1209104d..fcd78ebc9b9a 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 41a60cb518e7..96e29d05393a 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 4859f5dc8437..c3907b1af4f5 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 acbad0bc2a9e..47e45c4270aa 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 3023d63b8a87..f69d0a1ed05b 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 9ba5c9dd0c20..640f6daf6e85 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 7a3829209f4d..b84b1529c32c 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 5cae4594f7b6..49709436ab23 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 -{ - [Weight(30)] - public class PublishedStatusDashboard : IDashboard - { - public string Alias => "settingsPublishedStatus"; - - public string[] Sections => new [] { "settings" }; +namespace Umbraco.Cms.Core.Dashboards; - public string View => "views/dashboard/settings/publishedstatus.html"; +[Weight(30)] +public class PublishedStatusDashboard : IDashboard +{ + public string Alias => "settingsPublishedStatus"; - 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 15eb8836973f..25b064154b0c 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 e5f37fd5a316..b9cb57224099 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 544299b03a16..9648e76fcabf 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; + private readonly IEventMessagesAccessor _eventMessagesAccessor; - public DefaultEventMessagesFactory(IEventMessagesAccessor eventMessagesAccessor) - { - if (eventMessagesAccessor == null) throw new ArgumentNullException(nameof(eventMessagesAccessor)); - _eventMessagesAccessor = eventMessagesAccessor; - } + public DefaultEventMessagesFactory(IEventMessagesAccessor eventMessagesAccessor) + { + _eventMessagesAccessor = eventMessagesAccessor ?? throw new ArgumentNullException(nameof(eventMessagesAccessor)); + } - public EventMessages Get() + public EventMessages Get() + { + EventMessages? eventMessages = _eventMessagesAccessor.EventMessages; + if (eventMessages == null) { - var eventMessages = _eventMessagesAccessor.EventMessages; - if (eventMessages == null) - _eventMessagesAccessor.EventMessages = eventMessages = new EventMessages(); - return eventMessages; + _eventMessagesAccessor.EventMessages = eventMessages = new EventMessages(); } - public EventMessages? GetOrDefault() - { - return _eventMessagesAccessor.EventMessages; - } + return eventMessages; } + + public EventMessages? GetOrDefault() => _eventMessagesAccessor.EventMessages; } diff --git a/src/Umbraco.Core/DelegateEqualityComparer.cs b/src/Umbraco.Core/DelegateEqualityComparer.cs index 64d715c838a5..8a442e8f8547 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; + private readonly Func _equals; + private readonly Func _getHashcode; - #region Implementation of IEqualityComparer + #region Implementation of IEqualityComparer - public DelegateEqualityComparer(Func equals, Func getHashcode) - { - _getHashcode = getHashcode; - _equals = equals; - } + 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; - }); - } + 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) - { - return _equals.Invoke(x, y); - } + /// + /// 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) - { - return _getHashcode.Invoke(obj); - } + /// + /// 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 - } + #endregion } diff --git a/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs b/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs index d1fabe26dbbc..939315cd86e4 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 59f06801ffbd..2629aceb6fb3 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.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; } - - /// - /// 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(); - } + IServiceCollection Services { get; } + + IConfiguration Config { get; } + + TypeLoader TypeLoader { 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; } + + /// + /// 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 d0f198557f31..3d74261abf86 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) + 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 - { - 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 @@ public static void AddMultipleUnique( 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 @@ public static void AddUnique( 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 9bcc0cf7f81a..9c2202e2aaa6 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 - { - /// - /// 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); + /// 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); + /// + /// 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); - } + [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); } } diff --git a/src/Umbraco.Core/DependencyInjection/StaticServiceProvider.cs b/src/Umbraco.Core/DependencyInjection/StaticServiceProvider.cs index fdc4e3f6224b..6f8e4a2173f8 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 cba4a95c8e2b..bd8114bab9ab 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.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 11acf17a3bf4..5ad759de681b 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.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(); - } + // 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 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 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 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 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(); + /// + /// 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(); + 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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(); - } + /// + /// 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 81a1bbac326f..e3a659056bdb 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 bce6a36da907..90e2e49c9462 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 441bc836da8b..844c52a5ab0b 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs @@ -5,72 +5,80 @@ 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.AddNotificationHandler(); - return builder; - } + builder.Services.AddNotificationAsyncHandler(); + return builder; + } - /// - /// 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 - { - 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); - internal static IServiceCollection AddNotificationHandler(this IServiceCollection services) - where TNotificationHandler : INotificationHandler - where TNotification : INotification + if (!services.Contains(descriptor)) { - // 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; + services.Add(descriptor); } - 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); + return services; + } - if (!services.Contains(descriptor)) - { - services.Add(descriptor); - } + 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); - return services; + if (!services.Contains(descriptor)) + { + services.Add(descriptor); } + + return services; } } diff --git a/src/Umbraco.Core/DependencyInjection/UniqueServiceDescriptor.cs b/src/Umbraco.Core/DependencyInjection/UniqueServiceDescriptor.cs index 538f3f1dda30..57a8bcfe99d8 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) + { + } + + /// + 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() { - /// - /// Initializes a new instance of the class. - /// - public UniqueServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime) - : base(serviceType, implementationType, lifetime) + 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); + if (ImplementationInstance is not null) + { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ImplementationInstance); + } - /// - public override int GetHashCode() + if (ImplementationFactory 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(ImplementationFactory); } + + return hashCode; } } diff --git a/src/Umbraco.Core/Deploy/ArtifactBase.cs b/src/Umbraco.Core/Deploy/ArtifactBase.cs index 200b47096df8..cc2415f4cde1 100644 --- a/src/Umbraco.Core/Deploy/ArtifactBase.cs +++ b/src/Umbraco.Core/Deploy/ArtifactBase.cs @@ -21,8 +21,6 @@ protected ArtifactBase(TUdi udi, IEnumerable? dependencies = protected abstract string GetChecksum(); - #region Abstract implementation of IArtifactSignature - Udi IArtifactSignature.Udi => Udi; public TUdi Udi { get; set; } @@ -45,8 +43,6 @@ public IEnumerable Dependencies 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 618400e39547..07ba917dc2ef 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; - } + Udi = udi; + Ordering = ordering; + Mode = mode; + } - /// - /// Gets the entity id of the artifact that is a dependency. - /// - public Udi Udi { get; private set; } + /// + /// 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; private set; } + /// + /// Gets a value indicating whether the dependency is ordering. + /// + public bool Ordering { get; } - /// - /// Gets the dependency mode. - /// - public ArtifactDependencyMode Mode { get; private set; } - } + /// + /// 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 a5fff5380097..1be524c86f5e 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 Dictionary(); + private readonly Dictionary _dependencies = new(); - public IEnumerator GetEnumerator() - { - return _dependencies.Values.GetEnumerator(); - } + public int Count => _dependencies.Count; - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + public IEnumerator GetEnumerator() => _dependencies.Values.GetEnumerator(); - public void Add(ArtifactDependency item) + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(ArtifactDependency item) + { + if (_dependencies.ContainsKey(item.Udi)) { - 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(); - } + _dependencies[item.Udi] = item; + } - public bool Contains(ArtifactDependency item) - { - return _dependencies.ContainsKey(item.Udi) && - (_dependencies[item.Udi].Mode == item.Mode || _dependencies[item.Udi].Mode == ArtifactDependencyMode.Match); - } + public void Clear() => _dependencies.Clear(); - public void CopyTo(ArtifactDependency[] array, int arrayIndex) - { - _dependencies.Values.CopyTo(array, arrayIndex); - } + public bool Contains(ArtifactDependency item) => + _dependencies.ContainsKey(item.Udi) && + (_dependencies[item.Udi].Mode == item.Mode || _dependencies[item.Udi].Mode == ArtifactDependencyMode.Match); - public bool Remove(ArtifactDependency item) - { - throw new NotSupportedException(); - } + public void CopyTo(ArtifactDependency[] array, int arrayIndex) => _dependencies.Values.CopyTo(array, arrayIndex); - public int Count - { - get { return _dependencies.Count; } - } + public bool Remove(ArtifactDependency item) => throw new NotSupportedException(); - public bool IsReadOnly - { - get { return false; } - } - } + public bool IsReadOnly => false; } diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs index 7a2d108a1361..b997b9c75970 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 0849f3526fd9..1b75fe11c052 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 72724ee57b25..0ff1e20e8766 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; - } + Artifact = art; + Entity = entity; + Connector = connector; + NextPass = nextPass; + } - /// - /// Gets or sets the artifact. - /// - public new TArtifact Artifact { get; set; } + /// + /// Gets or sets the artifact. + /// + public new TArtifact Artifact { get; set; } - /// - /// Gets or sets the entity. - /// - public TEntity? Entity { get; set; } + /// + /// Gets or sets the entity. + /// + public TEntity? Entity { get; set; } - /// - protected sealed override IArtifact GetArtifactAsIArtifact() - { - return Artifact; - } - } + /// + protected sealed override IArtifact GetArtifactAsIArtifact() => Artifact; } diff --git a/src/Umbraco.Core/Deploy/ArtifactSignature.cs b/src/Umbraco.Core/Deploy/ArtifactSignature.cs index 629d65593c20..3dccddba2935 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(); - } + Udi = udi; + Checksum = checksum; + Dependencies = dependencies ?? Enumerable.Empty(); + } - public Udi Udi { get; private set; } + public Udi Udi { get; } - public string Checksum { get; private set; } + public string Checksum { get; } - public IEnumerable Dependencies { get; private set; } - } + public IEnumerable Dependencies { get; } } diff --git a/src/Umbraco.Core/Deploy/Difference.cs b/src/Umbraco.Core/Deploy/Difference.cs index be0c086c0b8f..d704642a9f9d 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) + { + 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() { - public Difference(string title, string? text = null, string? category = null) + 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 7a6ee5ae0977..30439380f222 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 5eb9c079f382..faea983dee8a 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 695624cd86a9..f1dd35295fcc 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 87a00e796907..6b91926b578c 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 c6e2da997b0f..bdc8fd8d61d4 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 6e582803a291..ed169b9df531 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 ef6c44e1e650..466c87a3edba 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 d19d2ad64a99..2ae2bb4bb919 100644 --- a/src/Umbraco.Core/Deploy/IFileTypeCollection.cs +++ b/src/Umbraco.Core/Deploy/IFileTypeCollection.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +public interface IFileTypeCollection { - public interface IFileTypeCollection - { - IFileType this[string entityType] { get; } + IFileType this[string entityType] { get; } - bool Contains(string entityType); - } + bool Contains(string entityType); } diff --git a/src/Umbraco.Core/Deploy/IImageSourceParser.cs b/src/Umbraco.Core/Deploy/IImageSourceParser.cs index 084ba1b11867..7b9e3f5e9618 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 5883f7321762..7ec3fff0fa3a 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 81b014c1cca6..1945b2bdb3c6 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 3f789e2e38e4..f6cd7c80024f 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); - - /// - /// Gets an artifact. - /// - /// The entity. - /// The corresponding artifact. - IArtifact GetArtifact(object entity); + /// The entity identifier of the artifact. + /// The corresponding artifact, or null. + IArtifact? GetArtifact(Udi udi); - /// - /// Initializes processing for an artifact. - /// - /// The artifact. - /// The deploy context. - /// The mapped artifact. - ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context); + /// + /// Gets an artifact. + /// + /// The entity. + /// The corresponding artifact. + IArtifact GetArtifact(object entity); - /// - /// Processes an artifact. - /// - /// The mapped artifact. - /// The deploy context. - /// The processing pass number. - void Process(ArtifactDeployState dart, IDeployContext context, int pass); + /// + /// Initializes processing for an artifact. + /// + /// The artifact. + /// The deploy context. + /// The mapped artifact. + ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context); - /// - /// 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); + /// + /// Processes an artifact. + /// + /// The mapped artifact. + /// The deploy context. + /// The processing pass number. + void Process(ArtifactDeployState dart, IDeployContext context, int pass); - /// - /// 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); + /// + /// 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 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); + /// + /// 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); - /// - /// 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 66364a08f36f..c68906bbbf1d 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 2c684f2ccd35..f2a776c7ca3a 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 988eaca78c63..304ff22c5a68 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 25f6e530e115..ac37c69f122d 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); + Directory.CreateDirectory(directory); + } - return bRet; + 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)) + { + return Write(marchal, stream.SafeFileHandle, options, withException); } } + } - public static bool Dump(IMarchal marchal, IHostingEnvironment hostingEnvironment, Option options = Option.WithFullMemory, bool withException = false) + // 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()) { - lock (LockO) + IntPtr currentProcessHandle = currentProcess.Handle; + var currentProcessId = (uint)currentProcess.Id; + + MiniDumpExceptionInformation exp; + + exp.ThreadId = GetCurrentThreadId(); + exp.ClientPointers = false; + exp.ExceptionPointers = IntPtr.Zero; + + if (withException) { - // 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); - } + 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) + public static bool OkToDump(IHostingEnvironment hostingEnvironment) + { + lock (LockO) { - lock (LockO) + var directory = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data + "/MiniDump"); + if (Directory.Exists(directory) == false) { - 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 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 273a4fb32c11..770aefd50f42 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 e8e3c620505e..380f7ee28719 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 40fbb1bad821..6cb2642b1539 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 44cc15033f96..de968f167640 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; - - /// - /// 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)); - } + _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 dictionary value based on the key supplied - /// - /// - /// - public string? this[string key] - { - get + /// + /// Returns the current culture + /// + public CultureInfo Culture => _specificCulture ?? Thread.CurrentThread.CurrentUICulture; + + 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; + } - var byLang = found.Translations?.FirstOrDefault(x => x.Language?.Equals(Language) ?? false); - if (byLang == null) - { - return string.Empty; + culture = culture.Parent; } - return byLang.Value; - } - } + return null; + }); - /// - /// 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 8713e338ea47..4c4eb030cc7e 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 -{ - /// - /// 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 ILocalizationService _localizationService; - private readonly AppCaches _appCaches; +namespace Umbraco.Cms.Core.Dictionary; - public DefaultCultureDictionaryFactory(ILocalizationService localizationService, AppCaches appCaches) - { - _localizationService = localizationService; - _appCaches = appCaches; - } +/// +/// 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 ICultureDictionary CreateDictionary() - { - return new DefaultCultureDictionary(_localizationService, _appCaches.RequestCache); - } + public DefaultCultureDictionaryFactory(ILocalizationService localizationService, AppCaches appCaches) + { + _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 152a3663fd80..874a00a4ac87 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 4304098324ad..6cc7f38d91e0 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(); + /// +#pragma warning disable CA1063 // Implement IDisposable Correctly + 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 - /// - /// True if disposing via Dispose method and not a finalizer. Always true for this class. - protected virtual void Dispose(bool disposing) + /// + /// 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 (!Disposed) + if (disposing) { - if (disposing) - { - DisposeResources(); - } - - Disposed = true; + DisposeResources(); } - } - /// -#pragma warning disable CA1063 // Implement IDisposable Correctly - public void Dispose() => Dispose(disposing: true); // We do not use GC.SuppressFinalize because this has no finalizer -#pragma warning restore CA1063 // Implement IDisposable Correctly + Disposed = true; + } } } diff --git a/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs b/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs index 01acd02c1067..8ae47fce08f5 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 2f27929a6c17..570af005b590 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 DistributedLockingException(string message) } /// - /// 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 9d6502379080..064a04680316 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 4d37238c0df9..8e21004cecfd 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 abf84470e05f..068684f31039 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 202bb594bcf1..261bd802e3b4 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 5df8a236509a..57252364d322 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 1bd1cfe206de..ecc1c99cfa01 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 d8bd73aca933..6ab0b76e3307 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; + _features = features; + Languages = languages; + } - public BackOfficePreviewModel(UmbracoFeatures features, IEnumerable languages) - { - _features = features; - Languages = languages; - } + public IEnumerable Languages { get; } - public IEnumerable Languages { get; } - public bool DisableDevicePreview => _features.Disabled.DisableDevicePreview; - public string? PreviewExtendedHeaderView => _features.Enabled.PreviewExtendedView; - } + 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 91bc3e191b42..a1c46cdb57e5 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 223778b79d3a..b7b5269ee7d0 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 a70509237a73..3e2b8995192b 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 17bb195e4b95..2f6bc9f110e5 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 -{ - // 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. +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 +{ /// - /// Provides a general object validator. + /// Gets the object type validated by this validator. /// - public interface IEditorValidator : IDiscoverable - { - /// - /// Gets the object type validated by this validator. - /// - Type ModelType { get; } + Type ModelType { get; } - /// - /// Validates an object. - /// - IEnumerable Validate(object model); - } + /// + /// Validates an object. + /// + IEnumerable Validate(object model); } diff --git a/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs b/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs index 23fc59da24bc..be9b05230f2e 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.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Editors +namespace Umbraco.Cms.Core.Editors; + +public class UserEditorAuthorizationHelper { - 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) { - private readonly IContentService _contentService; - private readonly IMediaService _mediaService; - private readonly IEntityService _entityService; - private readonly AppCaches _appCaches; + _contentService = contentService; + _mediaService = mediaService; + _entityService = entityService; + _appCaches = appCaches; + } - 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; - /// - /// 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) + // a) A non-admin cannot save an admin + if (savingUser != null) { - var currentIsAdmin = currentUser?.IsAdmin() ?? false; - - // a) A non-admin cannot save an admin - - if (savingUser != null) + if (savingUser.IsAdmin() && currentIsAdmin == false) { - if (savingUser.IsAdmin() && currentIsAdmin == false) - return Attempt.Fail("The current user is not an administrator so cannot save another administrator"); + 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 + // 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) + { + return pathResult; + } - if (currentIsAdmin) - return Attempt.Succeed(); + // 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()) - { - 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. + // 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"); + } - var newGroups = savingUser == null - ? savingGroupAliases - : savingGroupAliases.Except(savingUser.Groups.Select(x => x.Alias)).ToArray(); + // 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; + var userGroupsChanged = savingUser != null && newGroups.Length > 0; - if (userGroupsChanged) + 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) { - // 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) + { + 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"); + } + } + else { - if (contentId == Constants.System.Root) + IContent? content = _contentService.GetById(contentId); + if (content == null) { - 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"); + continue; } - else + + var hasAccess = currentUser.HasPathAccess(content, _entityService, _appCaches); + if (hasAccess == false) { - 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); + return Attempt.Fail("The current user does not have access to the content path " + + content.Path); } } } + } - if (startMediaIds != null) + if (startMediaIds != null) + { + foreach (var mediaId in startMediaIds) { - foreach (var mediaId in startMediaIds) + if (mediaId == Constants.System.Root) { - if (mediaId == Constants.System.Root) + var hasAccess = ContentPermissions.HasPathAccess( + "-1", + currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches), + Constants.System.RecycleBinMedia); + if (hasAccess == false) { - 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"); + return Attempt.Fail("The current user does not have access to the media root"); } - else + } + else + { + IMedia? media = _mediaService.GetById(mediaId); + if (media == null) { - 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); + 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(); } + + return Attempt.Succeed(); } } diff --git a/src/Umbraco.Core/Enum.cs b/src/Umbraco.Core/Enum.cs index 9ca1111a3044..6084dfe9719e 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() - { - Values = Enum.GetValues(typeof(T)).Cast().ToList(); - - 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; - } - } + IntToValue = new Dictionary(); + ValueToName = new Dictionary(); + SensitiveNameToValue = new Dictionary(); + InsensitiveNameToValue = new Dictionary(); - public static bool IsDefined(T value) + foreach (T value in Values) { - return ValueToName.Keys.Contains(value); - } + var name = value.ToString(); - public static bool IsDefined(string value) - { - return SensitiveNameToValue.Keys.Contains(value); + IntToValue[Convert.ToInt32(value)] = value; + ValueToName[value] = name!; + SensitiveNameToValue[name!] = value; + InsensitiveNameToValue[name!.ToLowerInvariant()] = value; } + } - public static bool IsDefined(int value) - { - return IntToValue.Keys.Contains(value); - } + public static bool IsDefined(T value) => ValueToName.Keys.Contains(value); - public static IEnumerable GetValues() - { - return Values; - } + public static bool IsDefined(string value) => SensitiveNameToValue.Keys.Contains(value); - public static string[] GetNames() - { - return ValueToName.Values.ToArray(); - } + public static bool IsDefined(int value) => IntToValue.Keys.Contains(value); - public static string? GetName(T value) - { - return ValueToName.TryGetValue(value, out var name) ? name : null; - } + public static IEnumerable GetValues() => Values; - public static T Parse(string value, bool ignoreCase = false) - { - var names = ignoreCase ? InsensitiveNameToValue : SensitiveNameToValue; - if (ignoreCase) value = value.ToLowerInvariant(); + public static string[] GetNames() => ValueToName.Values.ToArray(); - if (names.TryGetValue(value, out var parsed)) - return parsed; + public static string? GetName(T value) => ValueToName.TryGetValue(value, out var name) ? name : null; - throw new ArgumentException($"Value \"{value}\"is not a valid {typeof(T).Name} enumeration value.", nameof(value)); + public static T Parse(string value, bool ignoreCase = false) + { + Dictionary names = ignoreCase ? InsensitiveNameToValue : SensitiveNameToValue; + if (ignoreCase) + { + value = value.ToLowerInvariant(); } - public static bool TryParse(string value, out T returnValue, bool ignoreCase = false) + if (names.TryGetValue(value, out T parsed)) { - var names = ignoreCase ? InsensitiveNameToValue : SensitiveNameToValue; - if (ignoreCase) value = value.ToLowerInvariant(); - return names.TryGetValue(value, out returnValue); + return parsed; } - public static T? ParseOrNull(string value) + 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) { - if (string.IsNullOrWhiteSpace(value)) - return null; + value = value.ToLowerInvariant(); + } - if (InsensitiveNameToValue.TryGetValue(value.ToLowerInvariant(), out var parsed)) - return parsed; + return names.TryGetValue(value, out returnValue); + } + public static T? ParseOrNull(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { return null; } - public static T? CastOrNull(int value) + if (InsensitiveNameToValue.TryGetValue(value.ToLowerInvariant(), out T parsed)) { - if (IntToValue.TryGetValue(value, out var foundValue)) - return foundValue; + return parsed; + } - return null; + 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 097ffc962975..04b3bc91ff9e 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 c9958a5fc9a7..22c7ef4c7e22 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) + { + if (other is null || other.EventObject is null) + { + return false; + } - public bool Equals(CancellableEnumerableObjectEventArgs? other) + if (ReferenceEquals(this, other)) { - if (other is null || other.EventObject is null) return false; - if (ReferenceEquals(this, other)) return true; + return true; + } + + return EventObject?.SequenceEqual(other.EventObject) ?? false; + } - return EventObject?.SequenceEqual(other.EventObject) ?? false; + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; } - public override bool Equals(object? obj) + if (ReferenceEquals(this, obj)) { - 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 (obj.GetType() != GetType()) { - if (EventObject is not null) - { - return HashCodeHelper.GetHashCode(EventObject); - } + return false; + } + + return Equals((CancellableEnumerableObjectEventArgs)obj); + } - return base.GetHashCode(); + 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 a991f6532b44..7768da05f5cd 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 bool _cancel; - private IDictionary? _eventState; + private static readonly ReadOnlyDictionary EmptyAdditionalData = new(new Dictionary()); - private static readonly ReadOnlyDictionary EmptyAdditionalData = new ReadOnlyDictionary(new Dictionary()); + private bool _cancel; + private IDictionary? _eventState; - public CancellableEventArgs(bool canCancel, EventMessages messages, IDictionary additionalData) - { - CanCancel = canCancel; - Messages = messages; - AdditionalData = new ReadOnlyDictionary(additionalData); - } + public CancellableEventArgs(bool canCancel, EventMessages messages, IDictionary additionalData) + { + 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, EventMessages eventMessages) + { + CanCancel = canCancel; + Messages = eventMessages ?? throw new ArgumentNullException("eventMessages"); + AdditionalData = EmptyAdditionalData; + } - public CancellableEventArgs(bool canCancel) - { - CanCancel = canCancel; - //create a standalone messages - Messages = new EventMessages(); - AdditionalData = EmptyAdditionalData; - } + public CancellableEventArgs(bool canCancel) + { + CanCancel = canCancel; - public CancellableEventArgs(EventMessages eventMessages) - : this(true, eventMessages) - { } + // create a standalone messages + Messages = new EventMessages(); + AdditionalData = EmptyAdditionalData; + } - public CancellableEventArgs() - : this(true) - { } + public CancellableEventArgs(EventMessages eventMessages) + : this(true, eventMessages) + { + } - /// - /// Flag to determine if this instance will support being cancellable - /// - public bool CanCancel { get; set; } + 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 + /// + /// If this instance supports cancellation, this gets/sets the cancel value + /// + public bool Cancel + { + get { - 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 - { - if (CanCancel == false) - { - throw new InvalidOperationException("This event argument class does not support canceling."); - } - _cancel = value; - } - } - /// - /// 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); + return _cancel; } - /// - /// 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 + set { - get => _eventState ?? (_eventState = new Dictionary()); - set => _eventState = value; + if (CanCancel == false) + { + throw new InvalidOperationException("This event argument class does not support canceling."); + } + + _cancel = value; } + } + + /// + /// Returns the EventMessages object which is used to add messages to the message collection for this event + /// + public EventMessages Messages { get; } - public bool Equals(CancellableEventArgs? other) + /// + /// 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)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Equals(AdditionalData, other.AdditionalData); + 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() != GetType()) return false; - return Equals((CancellableEventArgs) obj); + return true; } - public override int GetHashCode() + 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 AdditionalData != null ? AdditionalData.GetHashCode() : 0; + return false; } - public static bool operator ==(CancellableEventArgs? left, CancellableEventArgs? right) + if (ReferenceEquals(this, obj)) { - return Equals(left, right); + return true; } - public static bool operator !=(CancellableEventArgs left, CancellableEventArgs right) + if (obj.GetType() != GetType()) { - return Equals(left, right) == false; + 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 2697b773c22f..26aa61b67a9e 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 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, 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, EventMessages eventMessages) + : this(eventObject, true, eventMessages) + { + } - protected CancellableObjectEventArgs(object? eventObject) - : this(eventObject, true) - { - } + protected CancellableObjectEventArgs(object? eventObject, bool canCancel) + : base(canCancel) => + EventObject = eventObject; - /// - /// 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) + : 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 939fd8e11be0..5d9865c253ae 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> { + 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) + { + } + /// - /// Represent event data, for events that support cancellation, and expose an impacted object. + /// Gets or sets the impacted object. /// - /// The type of the exposed, impacted object. - public class CancellableObjectEventArgs : CancellableObjectEventArgs, IEquatable> + /// + /// This is protected so that inheritors can expose it with their own name + /// + protected new TEventObject? EventObject { - public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) - : base(eventObject, canCancel, messages, additionalData) - { - } + get => (TEventObject?)base.EventObject; + set => base.EventObject = value; + } - public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel, EventMessages eventMessages) - : base(eventObject, canCancel, eventMessages) - { - } + public static bool operator ==( + CancellableObjectEventArgs left, + CancellableObjectEventArgs right) => Equals(left, right); - public CancellableObjectEventArgs(TEventObject? eventObject, EventMessages eventMessages) - : base(eventObject, eventMessages) - { - } + public static bool operator !=( + CancellableObjectEventArgs left, + CancellableObjectEventArgs right) => !Equals(left, right); - public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel) - : base(eventObject, canCancel) + public bool Equals(CancellableObjectEventArgs? other) + { + if (other is null) { + return false; } - public CancellableObjectEventArgs(TEventObject? eventObject) - : base(eventObject) + if (ReferenceEquals(this, other)) { + return true; } - /// - /// Gets or sets the impacted object. - /// - /// - /// This is protected so that inheritors can expose it with their own name - /// - protected new TEventObject? EventObject + return base.Equals(other) && EqualityComparer.Default.Equals(EventObject, other.EventObject); + } + + public override bool Equals(object? obj) + { + if (obj is null) { - get => (TEventObject?) base.EventObject; - set => base.EventObject = value; + return false; } - public bool Equals(CancellableObjectEventArgs? other) + if (ReferenceEquals(this, obj)) { - if (other is null) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && EqualityComparer.Default.Equals(EventObject, other.EventObject); + return true; } - public override bool Equals(object? obj) + if (obj.GetType() != GetType()) { - if (obj is null) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((CancellableObjectEventArgs)obj); + return false; } - public override int GetHashCode() + return Equals((CancellableObjectEventArgs)obj); + } + + public override int GetHashCode() + { + unchecked { - 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 78f714f75439..732e6f2452b4 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 6a4969710a76..bead8213b618 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; - } + 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 CopyEventArgs(TEntity eventObject, TEntity copy, int parentId) - : base(eventObject) + public bool Equals(CopyEventArgs? other) + { + if (ReferenceEquals(null, other)) { - Copy = copy; - ParentId = parentId; + return false; } - public CopyEventArgs(TEntity eventObject, TEntity copy, bool canCancel, int parentId, bool relateToOriginal) - : base(eventObject, canCancel) + if (ReferenceEquals(this, other)) { - Copy = copy; - ParentId = parentId; - RelateToOriginal = relateToOriginal; + return true; } - /// - /// The copied entity - /// - public TEntity Copy { get; set; } + return base.Equals(other) && EqualityComparer.Default.Equals(Copy, other.Copy) && + ParentId == other.ParentId && RelateToOriginal == other.RelateToOriginal; + } - /// - /// The original entity - /// - public TEntity? Original + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - get { return EventObject; } + return false; } - /// - /// 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 (ReferenceEquals(this, obj)) { - 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 true; } - public override bool Equals(object? obj) + if (obj.GetType() != GetType()) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((CopyEventArgs) obj); + return false; } - public override int GetHashCode() + return Equals((CopyEventArgs)obj); + } + + 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 1696e07ec66f..3ca366834f7e 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) - { - MediaFilesToDelete = new List(); - } + get => EventObject ?? Enumerable.Empty(); + set => EventObject = value; + } - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - /// - public DeleteEventArgs(IEnumerable eventObject, EventMessages eventMessages) : base(eventObject, eventMessages) - { - MediaFilesToDelete = new List(); - } + /// + /// 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; } - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public DeleteEventArgs(TEntity eventObject, EventMessages eventMessages) - : base(new List { eventObject }, eventMessages) - { - MediaFilesToDelete = new List(); - } + public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) => + Equals(left, right); - /// - /// Constructor accepting a single entity instance - /// - /// - /// - /// - public DeleteEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) - : base(new List { eventObject }, canCancel, eventMessages) + 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, bool canCancel) : base(eventObject, canCancel) + if (ReferenceEquals(this, other)) { - MediaFilesToDelete = new List(); + return true; } - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - public DeleteEventArgs(IEnumerable eventObject) : base(eventObject) - { - MediaFilesToDelete = new List(); - } + return base.Equals(other) && MediaFilesToDelete.SequenceEqual(other.MediaFilesToDelete); + } - /// - /// Constructor accepting a single entity instance - /// - /// - public DeleteEventArgs(TEntity eventObject) - : base(new List { eventObject }) + 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) - : base(new List { eventObject }, canCancel) + if (ReferenceEquals(this, obj)) { - MediaFilesToDelete = new List(); + return true; } - /// - /// Returns all entities that were deleted during the operation - /// - public IEnumerable DeletedEntities + if (obj.GetType() != GetType()) { - get => EventObject ?? Enumerable.Empty(); - set => EventObject = value; + return false; } - /// - /// 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; } + return Equals((DeleteEventArgs)obj); + } - public bool Equals(DeleteEventArgs? other) + public override int GetHashCode() + { + unchecked { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && MediaFilesToDelete.SequenceEqual(other.MediaFilesToDelete); + return (base.GetHashCode() * 397) ^ MediaFilesToDelete.GetHashCode(); } + } - 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 static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) => + !Equals(left, right); +} - public override int GetHashCode() - { - unchecked - { - 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 static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) - { - return Equals(left, right); - } + public DeleteEventArgs(int id, bool canCancel) + : base(canCancel) => + Id = id; - public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) - { - return !Equals(left, right); - } - } + public DeleteEventArgs(int id) => Id = id; - public class DeleteEventArgs : CancellableEventArgs, IEquatable - { - public DeleteEventArgs(int id, bool canCancel, EventMessages eventMessages) - : base(canCancel, eventMessages) - { - Id = id; - } + /// + /// Gets the Id of the object being deleted. + /// + public int Id { get; } - public DeleteEventArgs(int id, bool canCancel) - : base(canCancel) + public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) => Equals(left, right); + + public bool Equals(DeleteEventArgs? other) + { + if (ReferenceEquals(null, other)) { - Id = id; + return false; } - public DeleteEventArgs(int id) + if (ReferenceEquals(this, other)) { - Id = id; + return true; } - /// - /// Gets the Id of the object being deleted. - /// - public int Id { get; private set; } + return base.Equals(other) && Id == other.Id; + } - public bool Equals(DeleteEventArgs? 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) && 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; - if (obj.GetType() != this.GetType()) return false; - return Equals((DeleteEventArgs) obj); + return true; } - public override int GetHashCode() + if (obj.GetType() != GetType()) { - unchecked - { - return (base.GetHashCode() * 397) ^ Id; - } + return false; } - public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) - { - return Equals(left, right); - } + return Equals((DeleteEventArgs)obj); + } - public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) + public override int GetHashCode() + { + unchecked { - return !Equals(left, right); + 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 e27c155ec4a1..d298f5bbec60 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 -{ - /// - /// Contains types and methods that allow publishing general notifications. - /// - public partial class EventAggregator : IEventAggregator - { - private static readonly ConcurrentDictionary s_notificationAsyncHandlers - = new ConcurrentDictionary(); - - private static readonly ConcurrentDictionary s_notificationHandlers - = new ConcurrentDictionary(); +namespace Umbraco.Cms.Core.Events; - 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); - } +/// +/// Contains types and methods that allow publishing general notifications. +/// +public partial class EventAggregator : IEventAggregator +{ + private static readonly ConcurrentDictionary NotificationAsyncHandlers + = new(); - 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 static readonly ConcurrentDictionary NotificationHandlers = new(); - private async Task PublishCoreAsync( - IEnumerable> allHandlers, - INotification notification, - CancellationToken cancellationToken) - { - foreach (Func handler in allHandlers) - { - await handler(notification, cancellationToken).ConfigureAwait(false); - } - } - - private void PublishCore( - IEnumerable> allHandlers, - INotification notification) - { - foreach (Action handler in allHandlers) + private Task PublishNotificationAsync(INotification notification, CancellationToken cancellationToken = default) + { + Type notificationType = notification.GetType(); + NotificationAsyncHandlerWrapper asyncHandler = NotificationAsyncHandlers.GetOrAdd( + notificationType, + t => { - handler(notification); - } - } + 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); } - internal abstract class NotificationHandlerWrapper + private void PublishNotification(INotification notification) { - public abstract void Handle( - INotification notification, - ServiceFactory serviceFactory, - Action>, INotification> publish); + Type notificationType = notification.GetType(); + NotificationHandlerWrapper? asyncHandler = 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); } - internal abstract class NotificationAsyncHandlerWrapper + private async Task PublishCoreAsync( + IEnumerable> allHandlers, + INotification notification, + CancellationToken cancellationToken) { - public abstract Task HandleAsync( - INotification notification, - CancellationToken cancellationToken, - ServiceFactory serviceFactory, - Func>, INotification, CancellationToken, Task> publish); + foreach (Func handler in allHandlers) + { + await handler(notification, cancellationToken).ConfigureAwait(false); + } } - internal class NotificationAsyncHandlerWrapperImpl : NotificationAsyncHandlerWrapper - where TNotification : INotification + private void PublishCore( + IEnumerable> allHandlers, + INotification notification) { - /// - /// - /// 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); + 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 +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) { - /// - /// 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); - } + // 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 5bf54b516a50..277b24eb061e 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); + /// The type to return. + /// The service factory. + /// The new instance. + public static T GetInstance(this ServiceFactory factory) + => (T)factory(typeof(T)); - /// - public partial class EventAggregator : IEventAggregator + /// + /// 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 { - 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) { - // TODO: Introduce codegen efficient Guard classes to reduce noise. - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - PublishNotification(notification); - return PublishNotificationAsync(notification, cancellationToken); + throw new ArgumentNullException(nameof(notification)); } - /// - public void Publish(TNotification notification) - where TNotification : INotification + 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) { - // 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); - } + throw new ArgumentNullException(nameof(notification)); } - public bool PublishCancelable(TCancelableNotification notification) - where TCancelableNotification : ICancelableNotification + PublishNotification(notification); + Task task = PublishNotificationAsync(notification); + if (task is not null) { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - Publish(notification); - return notification.Cancel; + Task.WaitAll(task); } + } - public async Task PublishCancelableAsync(TCancelableNotification notification) - where TCancelableNotification : ICancelableNotification + public bool PublishCancelable(TCancelableNotification notification) + where TCancelableNotification : ICancelableNotification + { + if (notification == null) { - if (notification is null) - { - throw new ArgumentNullException(nameof(notification)); - } - - Task? task = PublishAsync(notification); - if (task is not null) - { - await task; - } - - return notification.Cancel; + throw new ArgumentNullException(nameof(notification)); } + + Publish(notification); + return notification.Cancel; } - /// - /// Extensions for . - /// - public static class ServiceFactoryExtensions + public async Task PublishCancelableAsync(TCancelableNotification notification) + where TCancelableNotification : ICancelableNotification { - /// - /// 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)); - - /// - /// 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)); + 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 aa6f2899cd63..3f7cd382ed1c 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 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; - } + private readonly EventArgs _args; + private readonly object _sender; + private readonly EventHandler _trackedEvent; - public override void RaiseEvent() - { - if (_trackedEvent != null) - { - _trackedEvent(_sender, _args); - } - } + public EventDefinition(EventHandler trackedEvent, object sender, EventArgs args, string? eventName = null) + : base(sender, args, eventName) + { + _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; + _trackedEvent?.Invoke(_sender, _args); + } +} - public EventDefinition(EventHandler trackedEvent, object sender, TEventArgs args, string? eventName = null) - : base(sender, args, eventName) - { - _trackedEvent = trackedEvent; - _sender = sender; - _args = args; - } +public class EventDefinition : EventDefinitionBase +{ + private readonly TEventArgs _args; + private readonly object _sender; + private readonly EventHandler _trackedEvent; - public override void RaiseEvent() - { - if (_trackedEvent != null) - { - _trackedEvent(_sender, _args); - } - } + public EventDefinition(EventHandler trackedEvent, object sender, TEventArgs args, string? eventName = null) + : base(sender, args, eventName) + { + _trackedEvent = trackedEvent; + _sender = sender; + _args = args; } - public class EventDefinition : EventDefinitionBase + public override void RaiseEvent() { - private readonly TypedEventHandler _trackedEvent; - private readonly TSender _sender; - private readonly TEventArgs _args; + _trackedEvent?.Invoke(_sender, _args); + } +} - public EventDefinition(TypedEventHandler trackedEvent, TSender sender, TEventArgs args, string? eventName = null) - : base(sender, args, eventName) - { - _trackedEvent = trackedEvent; - _sender = sender; - _args = args; - } +public class EventDefinition : EventDefinitionBase +{ + private readonly TEventArgs _args; + private readonly TSender _sender; + private readonly TypedEventHandler _trackedEvent; - public override void RaiseEvent() - { - if (_trackedEvent != null) - { - _trackedEvent(_sender, _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() + { + _trackedEvent?.Invoke(_sender, _args); } } diff --git a/src/Umbraco.Core/Events/EventDefinitionBase.cs b/src/Umbraco.Core/Events/EventDefinitionBase.cs index 422392423496..8ac84c470d1f 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; + + if (EventName.IsNullOrWhiteSpace()) { - Sender = sender ?? throw new ArgumentNullException(nameof(sender)); - Args = args ?? throw new ArgumentNullException(nameof(args)); - EventName = eventName; + // don't match "Ing" suffixed names + Attempt findResult = + EventNameExtractor.FindEvent(sender, args, EventNameExtractor.MatchIngNames); - if (EventName.IsNullOrWhiteSpace()) + 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); } + + EventName = findResult.Result?.Name; } + } - public object Sender { get; } - public object Args { get; } - public string? EventName { get; } + public object Sender { get; } - public abstract void RaiseEvent(); + public bool Equals(EventDefinitionBase? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } - public bool Equals(EventDefinitionBase? other) + if (ReferenceEquals(this, 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); + return true; } - public override bool Equals(object? obj) + 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)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((EventDefinitionBase) obj); + return false; } - public override int GetHashCode() + if (ReferenceEquals(this, obj)) { - unchecked - { - var hashCode = Args.GetHashCode(); - if (EventName is not null) - { - hashCode = (hashCode * 397) ^ EventName.GetHashCode(); - } - hashCode = (hashCode * 397) ^ Sender.GetHashCode(); - return hashCode; - } + return true; } - public static bool operator ==(EventDefinitionBase left, EventDefinitionBase right) + if (obj.GetType() != GetType()) { - return Equals(left, right); + return false; } - public static bool operator !=(EventDefinitionBase left, EventDefinitionBase right) + return Equals((EventDefinitionBase)obj); + } + + public override int GetHashCode() + { + unchecked { - return Equals(left, right) == false; + 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 47b0f9a44ea6..4872b23e8b84 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 4d98cbbcca95..6d9fd8103b2c 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 { + // keep these two for backward compatibility reasons but understand that + // they are *not* part of any scope / event dispatcher / anything... + /// - /// Extension methods for cancellable event operations + /// Raises a cancelable event and returns a value indicating whether the event should be cancelled. /// - public static class EventExtensions + /// 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 { - // 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 + 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 eef0985c23bd..8ba2c98bf804 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; - } + Category = category; + Message = message; + MessageType = messageType; + } - public string Category { get; private set; } - public string Message { get; private set; } - public EventMessageType MessageType { get; private set; } + public string Category { 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; } - } + 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 afbed0d590d9..a3c6ebf2f95b 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 23b40118c768..68d19f27fd15 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 c74d2e293e55..16f772dcb231 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 => + 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) { - if (x.GenericArgs.Length == 1) return true; + return true; + } - //special case for our own TypedEventHandler - if (x.EventInfo.EventHandlerType?.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) && x.GenericArgs.Length == 2) - { - return true; - } + // special case for our own TypedEventHandler + if (x.EventInfo.EventHandlerType?.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) && + x.GenericArgs.Length == 2) + { + return true; + } - return false; - }) - .ToArray(); - }); + return false; + }) + .ToArray(); + }); - return events.Where(x => + return events.Where(x => + { + if (x.GenericArgs.Length == 1 && x.GenericArgs[0] == tuple.Item2) { - if (x.GenericArgs.Length == 1 && x.GenericArgs[0] == tuple.Item2) - return true; + 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; - } + // 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 false; + }).Select(x => x.EventInfo.Name).ToArray(); + }); - return found.Where(x => exclude(x) == 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); - } + /// + /// 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) + /// + /// 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) + return words[0].EndsWith("ing"); + } + + /// + /// 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(@"(? - /// 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(); + public EventInfo EventInfo { get; } - /// - /// 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[]>(); + public Type[] GenericArgs { get; } } } diff --git a/src/Umbraco.Core/Events/EventNameExtractorError.cs b/src/Umbraco.Core/Events/EventNameExtractorError.cs index 8a506f907114..a2fd01f1c1df 100644 --- a/src/Umbraco.Core/Events/EventNameExtractorError.cs +++ b/src/Umbraco.Core/Events/EventNameExtractorError.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public enum EventNameExtractorError { - public enum EventNameExtractorError - { - NoneFound, - Ambiguous - } + NoneFound, + Ambiguous, } diff --git a/src/Umbraco.Core/Events/EventNameExtractorResult.cs b/src/Umbraco.Core/Events/EventNameExtractorResult.cs index 2f5498c33f28..fb847a02821a 100644 --- a/src/Umbraco.Core/Events/EventNameExtractorResult.cs +++ b/src/Umbraco.Core/Events/EventNameExtractorResult.cs @@ -1,18 +1,12 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public class EventNameExtractorResult { - public class EventNameExtractorResult - { - public EventNameExtractorError? Error { get; private set; } - public string? Name { get; private set; } + public EventNameExtractorResult(string? name) => Name = name; + + public EventNameExtractorResult(EventNameExtractorError? error) => Error = error; - public EventNameExtractorResult(string? name) - { - Name = name; - } + public EventNameExtractorError? Error { get; } - public EventNameExtractorResult(EventNameExtractorError? error) - { - Error = error; - } - } + public string? Name { get; } } diff --git a/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs b/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs index 2026f41ff300..06b7ff81f4bc 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 +namespace Umbraco.Cms.Core.Events; + +public class ExportedMemberEventArgs : EventArgs { - public class ExportedMemberEventArgs : EventArgs + public ExportedMemberEventArgs(IMember member, MemberExportModel exported) { - public IMember Member { get; } - public MemberExportModel Exported { get; } - - public ExportedMemberEventArgs(IMember member, MemberExportModel exported) - { - Member = member; - Exported = 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 9a6a4357e0d6..4aaeeac29de8 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 c654bb6c8619..379f532be22b 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 e3918113e1dc..d10b931548f6 100644 --- a/src/Umbraco.Core/Events/IEventDefinition.cs +++ b/src/Umbraco.Core/Events/IEventDefinition.cs @@ -1,11 +1,12 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public interface IEventDefinition { - public interface IEventDefinition - { - object Sender { get; } - object Args { get; } - string? EventName { get; } - - void RaiseEvent(); - } + 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 bef94b6d4a47..9d15a74c0241 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 - - /// - /// 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); + /// 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 cffff705da7e..e88ba73deedf 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 6abf6e8d4116..9ade74d20a5b 100644 --- a/src/Umbraco.Core/Events/IEventMessagesFactory.cs +++ b/src/Umbraco.Core/Events/IEventMessagesFactory.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// Event messages factory +/// +public interface IEventMessagesFactory { - /// - /// Event messages factory - /// - public interface IEventMessagesFactory - { - EventMessages Get(); + EventMessages Get(); - EventMessages? GetOrDefault(); - } + EventMessages? GetOrDefault(); } diff --git a/src/Umbraco.Core/Events/INotificationAsyncHandler.cs b/src/Umbraco.Core/Events/INotificationAsyncHandler.cs index cdcc21542f2a..25a46ed250d5 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 548bec39b891..2111009faabb 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 58fdafc341ed..89962bbb9c9a 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 8d0e8dbfe1f9..876f7b99eb62 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 2f6505635333..312f1b8146c6 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; + throw new ArgumentException("moveInfo argument must contain at least one item"); } - /// - /// 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; - MoveInfoCollection = moveInfo; - //assign the legacy props - EventObject = moveInfo.First().Entity; - } + // 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) + /// + /// 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) { - 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; + throw new ArgumentException("moveInfo argument must contain at least one item"); } - /// - /// 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) + 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) { - if (moveInfo.FirstOrDefault() is null) - { - throw new ArgumentException("moveInfo argument must contain at least one item"); - } + throw new ArgumentException("moveInfo argument must contain at least one item"); + } + + MoveInfoCollection = moveInfo; - MoveInfoCollection = moveInfo; - //assign the legacy props - EventObject = moveInfo.First().Entity; + // 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 + /// + /// Gets all MoveEventInfo objects used to create the object + /// + public IEnumerable>? MoveInfoCollection + { + get => _moveInfoCollection; + set { - get { return _moveInfoCollection; } - set + MoveEventInfo? first = value?.FirstOrDefault(); + if (first is null) { - var first = value?.FirstOrDefault(); - if (first is null) - { - throw new InvalidOperationException("MoveInfoCollection must have at least one item"); - } + throw new InvalidOperationException("MoveInfoCollection must have at least one item"); + } - _moveInfoCollection = value; + _moveInfoCollection = value; - //assign the legacy props - EventObject = first.Entity; - } + // assign the legacy props + EventObject = first.Entity; } + } + + public static bool operator ==(MoveEventArgs left, MoveEventArgs right) => Equals(left, right); - public bool Equals(MoveEventArgs? other) + public bool Equals(MoveEventArgs? other) + { + if (other is null) { - if (other is null) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && (MoveInfoCollection?.Equals(other.MoveInfoCollection) ?? 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((MoveEventArgs) obj); + return true; } - public override int GetHashCode() + return base.Equals(other) && (MoveInfoCollection?.Equals(other.MoveInfoCollection) ?? false); + } + + public override bool Equals(object? obj) + { + if (obj is null) { - unchecked - { - if (MoveInfoCollection is not null) - { - return (base.GetHashCode() * 397) ^ MoveInfoCollection.GetHashCode(); - } + return false; + } - return base.GetHashCode() * 397; - } + if (ReferenceEquals(this, obj)) + { + return true; } - public static bool operator ==(MoveEventArgs left, MoveEventArgs right) + if (obj.GetType() != GetType()) { - return Equals(left, right); + return false; } - public static bool operator !=(MoveEventArgs left, MoveEventArgs right) + return Equals((MoveEventArgs)obj); + } + + public override int GetHashCode() + { + unchecked { - return !Equals(left, right); + 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 126a3fd23003..92c09c92a8bf 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 d3e8436d0ee1..0db72488aa45 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) - { - Alias = alias; - ParentId = parentId; - } + public NewEventArgs(TEntity eventObject, string alias, int parentId, EventMessages eventMessages) + : base(eventObject, 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, string alias, TEntity? parent, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + Alias = alias; + Parent = parent; + } - public NewEventArgs(TEntity eventObject, string @alias, int parentId, EventMessages eventMessages) - : base(eventObject, eventMessages) - { - Alias = alias; - ParentId = parentId; - } + public NewEventArgs(TEntity eventObject, bool canCancel, string alias, int parentId) + : base(eventObject, canCancel) + { + 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, 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; } - public NewEventArgs(TEntity eventObject, bool canCancel, string @alias, int parentId) : base(eventObject, canCancel) - { - Alias = alias; - ParentId = parentId; - } + /// + /// Gets or Sets the Id of the parent. + /// + public int ParentId { get; } - public NewEventArgs(TEntity eventObject, bool canCancel, string @alias, TEntity? parent) - : base(eventObject, canCancel) - { - Alias = alias; - Parent = parent; - } + /// + /// Gets or Sets the parent IContent object. + /// + public TEntity? Parent { get; } - public NewEventArgs(TEntity eventObject, string @alias, int parentId) : base(eventObject) - { - Alias = alias; - ParentId = parentId; - } + public static bool operator ==(NewEventArgs left, NewEventArgs right) => Equals(left, right); - public NewEventArgs(TEntity eventObject, string @alias, TEntity? parent) - : base(eventObject) + public bool Equals(NewEventArgs? other) + { + if (ReferenceEquals(null, other)) { - Alias = alias; - Parent = parent; + return false; } - /// - /// The entity being created - /// - public TEntity? Entity + if (ReferenceEquals(this, other)) { - get { return EventObject; } + return true; } - /// - /// 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; } + return base.Equals(other) && string.Equals(Alias, other.Alias) && + EqualityComparer.Default.Equals(Parent, other.Parent) && ParentId == other.ParentId; + } - public bool Equals(NewEventArgs? 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) && string.Equals(Alias, other.Alias) && EqualityComparer.Default.Equals(Parent, other.Parent) && ParentId == other.ParentId; + 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((NewEventArgs?) obj); + return true; } - public override int GetHashCode() + if (obj.GetType() != GetType()) { - unchecked - { - 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; - } + return false; } - public static bool operator ==(NewEventArgs left, NewEventArgs right) - { - return Equals(left, right); - } + return Equals((NewEventArgs?)obj); + } - public static bool operator !=(NewEventArgs left, NewEventArgs right) + public override int GetHashCode() + { + unchecked { - return !Equals(left, right); + var 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; } } + + 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 a36368ea54b1..20398502a18b 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 80b6dcd8c7d5..8a48a0cfa90b 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 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, 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 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, 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 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; - /// - /// Constructor accepting a single entity instance - /// - /// - public PublishEventArgs(TEntity eventObject) - : base(new List { 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 a single entity instance - /// - /// - /// - /// - public PublishEventArgs(TEntity eventObject, bool canCancel, bool isAllPublished) - : base(new List { eventObject }, canCancel) + if (ReferenceEquals(this, other)) { + return true; } - /// - /// Returns all entities that were published during the operation - /// - public IEnumerable? PublishedEntities => EventObject; + return base.Equals(other); + } - public bool Equals(PublishEventArgs? 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); + 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((PublishEventArgs) obj); + return true; } - public override int GetHashCode() + if (obj.GetType() != GetType()) { - unchecked - { - return (base.GetHashCode() * 397); - } + return false; } - public static bool operator ==(PublishEventArgs left, PublishEventArgs right) - { - return Equals(left, right); - } + return Equals((PublishEventArgs)obj); + } - public static bool operator !=(PublishEventArgs left, PublishEventArgs right) + public override int GetHashCode() + { + unchecked { - 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 e79cd67cd8f7..bc8eac29a15e 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; - } + private readonly MediaFileManager _mediaFileManager; + + public QueuingEventDispatcher(MediaFileManager mediaFileManager) + : base(true) => + _mediaFileManager = mediaFileManager; - protected override void ScopeExitCompleted() + 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 (IEventDefinition e in GetEvents(EventDefinitionFilter.LastIn)) { - // 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 + e.RaiseEvent(); - 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 71b7647b4f9b..c259e271e54b 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; + if (eventHandler == null) + { + return args.Cancel; + } - protected QueuingEventDispatcherBase(bool raiseCancelable) + if (_raiseCancelable == false) { - _raiseCancelable = raiseCancelable; + return args.Cancel; } - private List Events => _events ?? (_events = new List()); + eventHandler(sender, args); + return args.Cancel; + } - public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? eventName = null) + 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 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(TypedEventHandler eventHandler, TSender 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 void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? eventName = null) + { + if (eventHandler == null) { - if (eventHandler == null) return; - Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + return; } - public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } + + public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) + { + if (eventHandler == null) { - if (eventHandler == null) return; - Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + return; } - public IEnumerable GetEvents(EventDefinitionFilter filter) + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } + + public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) + { + if (eventHandler == null) { - if (_events == null) - return Enumerable.Empty(); + return; + } - 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); - } + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } - return FilterSupersededAndUpdateToLatestEntity(events); + public IEnumerable GetEvents(EventDefinitionFilter filter) + { + if (_events == null) + { + return Enumerable.Empty(); } - private class EventDefinitionInfos + IReadOnlyList events; + switch (filter) { - public IEventDefinition? EventDefinition { get; set; } - public Type[]? SupersedeTypes { get; set; } + case EventDefinitionFilter.All: + events = _events; + break; + case EventDefinitionFilter.FirstIn: + var l1 = new OrderedHashSet(); + foreach (IEventDefinition e in _events) + { + l1.Add(e); + } + + events = l1; + break; + case EventDefinitionFilter.LastIn: + var l2 = new OrderedHashSet(false); + foreach (IEventDefinition e in _events) + { + l2.Add(e); + } + + events = l2; + break; + default: + throw new ArgumentOutOfRangeException("filter", filter, null); } - // 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? + return FilterSupersededAndUpdateToLatestEntity(events); + } + + 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 // - // 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) + // 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--) { - // 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 { - var def = events[index]; + EventDefinition = def, + SupersedeTypes = argTypeSuperceeding[def.Args.GetType()], + }; - var infos = new EventDefinitionInfos + 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 + IList? eventObjects = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject); + if (eventObjects == null) { - EventDefinition = def, - SupersedeTypes = argTypeSuperceeding[def.Args.GetType()] - }; + // 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; + } - var args = def.Args as CancellableObjectEventArgs; - if (args == null) - { - // not a cancellable event arg, include event definition in result - result.Add(def); + // 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 { - // 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) + // enumerable of objects + var toRemove = new List(); + foreach (var eventObject in eventObjects) { - // 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) + // 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) { - 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) + // found = must be removed, else track + if (IsSuperceeded(eventEntity, infos, entities)) { - // track - entities.Add(Tuple.Create(eventEntity, infos)); - - // track result arguments - // include event definition in result - resultArgs.Add(args); - result.Add(def); + toRemove.Add(eventEntity); } - } - else - { - // enumerable of objects - var toRemove = new List(); - foreach (var eventObject in eventObjects) + else { - // 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)); + entities.Add(Tuple.Create(eventEntity, infos)); } + } - // remove superseded entities - foreach (var entity in toRemove) - eventObjects.Remove(entity); + // 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 there are still entities in the list, keep the event definition + if (eventObjects.Count > 0) + { + if (toRemove.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); + // re-assign if changed + args.EventObject = eventObjects; } + + // track result arguments + // include event definition in result + resultArgs.Add(args); + result.Add(def); } } } + } - // go over all args in result, and update them with the latest instanceof each entity - UpdateToLatestEntities(entities, resultArgs); + // 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(); + // reverse, since we processed the list in reverse + result.Reverse(); - return result; - } + 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) + // 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)) { - // 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); + latestEntities.Add(entity.Item1); + } - foreach (var arg in args) + 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) { - // 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); - 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) + IEntity? foundEntity = latestEntities.FirstOrDefault(x => Equals(x, arg.EventObject)); + if (foundEntity != 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; + arg.EventObject = foundEntity; } - else + } + else + { + // enumerable of objects + // same as above but for each object + var updated = false; + for (var i = 0; i < eventObjects.Count; i++) { - // 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) { - var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, eventObjects[i])); - if (foundEntity == null) continue; - eventObjects[i] = foundEntity; - updated = true; + continue; } - if (updated) - arg.EventObject = eventObjects; + eventObjects[i] = foundEntity; + updated = true; + } + + if (updated) + { + arg.EventObject = eventObjects; } } } + } - // 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) + // 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) { - //var argType = meta.EventArgsType; - var 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) - 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) - { - // 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; - } - else - { - // non-generic, can compare types 1:1 - var supercededBy = superceeding.FirstOrDefault(x => - x.Item2.SupersedeTypes?.Any(y => y == argType) ?? false); - return supercededBy != null; - } + return false; } - public void ScopeExit(bool completed) + // delete event args does NOT supersedes 'unpublished' event + if ((argType?.IsGenericType ?? false) && argType.GetGenericTypeDefinition() == typeof(PublishEventArgs<>) && + infos.EventDefinition?.EventName == "Unpublished") { - if (_events == null) return; - if (completed) - ScopeExitCompleted(); - _events.Clear(); + return false; } - protected abstract void ScopeExitCompleted(); + // 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 ee0d43a07a6b..44fb13016b55 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, EventMessages eventMessages) + : base(true, eventMessages) => + NodeObjectType = nodeObjectType; - public RecycleBinEventArgs(Guid nodeObjectType) - : base(true) - { - 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; } - /// - /// Gets the Id of the node object type of the items - /// being deleted from the Recycle Bin. - /// - public Guid NodeObjectType { get; } + /// + /// Boolean indicating whether this event was fired for the Content's Recycle Bin. + /// + public bool IsContentRecycleBin => NodeObjectType == Constants.ObjectTypes.Document; - /// - /// Boolean indicating whether the Recycle Bin was emptied successfully - /// - public bool RecycleBinEmptiedSuccessfully { get; set; } + /// + /// Boolean indicating whether this event was fired for the Media's Recycle Bin. + /// + public bool IsMediaRecycleBin => NodeObjectType == Constants.ObjectTypes.Media; - /// - /// Boolean indicating whether this event was fired for the Content's Recycle Bin. - /// - public bool IsContentRecycleBin => NodeObjectType == Constants.ObjectTypes.Document; + public static bool operator ==(RecycleBinEventArgs left, RecycleBinEventArgs right) => Equals(left, right); - /// - /// 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) + { + if (ReferenceEquals(null, other)) + { + return false; + } - public bool Equals(RecycleBinEventArgs? other) + if (ReferenceEquals(this, other)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && NodeObjectType.Equals(other.NodeObjectType) && RecycleBinEmptiedSuccessfully == other.RecycleBinEmptiedSuccessfully; + return true; } - public override bool Equals(object? obj) + return base.Equals(other) && NodeObjectType.Equals(other.NodeObjectType) && + RecycleBinEmptiedSuccessfully == other.RecycleBinEmptiedSuccessfully; + } + + 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((RecycleBinEventArgs) obj); + return false; } - public override int GetHashCode() + if (ReferenceEquals(this, obj)) { - unchecked - { - int hashCode = base.GetHashCode(); - hashCode = (hashCode * 397) ^ NodeObjectType.GetHashCode(); - hashCode = (hashCode * 397) ^ RecycleBinEmptiedSuccessfully.GetHashCode(); - return hashCode; - } + return true; } - public static bool operator ==(RecycleBinEventArgs left, RecycleBinEventArgs right) + if (obj.GetType() != GetType()) { - return Equals(left, right); + return false; } - public static bool operator !=(RecycleBinEventArgs left, RecycleBinEventArgs right) + return Equals((RecycleBinEventArgs)obj); + } + + 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 c41043a03996..00302e9f3527 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 f37d8723a7fc..3817f93f6f4c 100644 --- a/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs +++ b/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs @@ -5,48 +5,49 @@ 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; - } - - 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}'"); + 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, + 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 a4fb6c3d18f5..a96de0671397 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 96b67ba769fb..d23ac75f9aca 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 3424962a54b9..319a0726f299 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 multiple entities that are used in the saving operation + /// + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { + } - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public SaveEventArgs(TEntity eventObject, EventMessages eventMessages) - : base(new List { eventObject }, 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 eventMessages) - : base(new List { eventObject }, canCancel, 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 multiple entities that are used in the saving operation - /// - /// - /// - public SaveEventArgs(IEnumerable eventObject, bool canCancel) - : base(eventObject, canCancel) - { - } + /// + /// 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) - : base(eventObject) - { - } + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, bool canCancel) + : base(eventObject, canCancel) + { + } - /// - /// Constructor accepting a single entity instance - /// - /// - public SaveEventArgs(TEntity eventObject) - : base(new List { eventObject }) - { - } + /// + /// 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, bool canCancel) - : base(new List { eventObject }, canCancel) - { - } + /// + /// Constructor accepting a single entity instance + /// + /// + public SaveEventArgs(TEntity eventObject) + : base(new List { eventObject }) + { + } - /// - /// Returns all entities that were saved during the operation - /// - public IEnumerable? SavedEntities => 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 cdd8707a79f9..6681d321b728 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)); - } + return false; + } - if (_isSuppressed) - { - return false; - } + _eventAggregator.Publish(notification); + return notification.Cancel; + } - _eventAggregator.Publish(notification); - return notification.Cancel; + public async Task PublishCancelableAsync(ICancelableNotification notification) + { + if (notification == null) + { + throw new ArgumentNullException(nameof(notification)); } - public async Task PublishCancelableAsync(ICancelableNotification notification) + if (_isSuppressed) { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } + return false; + } - if (_isSuppressed) - { - return false; - } + Task task = _eventAggregator.PublishAsync(notification); + if (task is not null) + { + await task; + } - var task = _eventAggregator.PublishAsync(notification); - if (task is not null) - { - await task; - } + return notification.Cancel; + } - return notification.Cancel; + public void Publish(INotification notification) + { + if (notification == null) + { + 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; } - public void ScopeExit(bool completed) + _notificationOnScopeCompleted.Add(notification); + } + + public void ScopeExit(bool completed) + { + try { - try + if (completed) { - if (completed) + foreach (INotification notification in _notificationOnScopeCompleted) { - foreach (INotification notification in _notificationOnScopeCompleted) - { - _eventAggregator.Publish(notification); - } + _eventAggregator.Publish(notification); } } - finally - { - _notificationOnScopeCompleted.Clear(); - } } + finally + { + _notificationOnScopeCompleted.Clear(); + } + } - public IDisposable Suppress() + public IDisposable Suppress() + { + lock (_locker) { - lock(_locker) + if (_isSuppressed) { - if (_isSuppressed) - { - throw new InvalidOperationException("Notifications are already suppressed"); - } - return new Suppressor(this); + throw new InvalidOperationException("Notifications are already suppressed"); } + + return new Suppressor(this); } + } - private class Suppressor : IDisposable + private class Suppressor : IDisposable + { + private readonly ScopedNotificationPublisher _scopedNotificationPublisher; + private bool _disposedValue; + + public Suppressor(ScopedNotificationPublisher scopedNotificationPublisher) { - private bool _disposedValue; - private readonly ScopedNotificationPublisher _scopedNotificationPublisher; + _scopedNotificationPublisher = scopedNotificationPublisher; + _scopedNotificationPublisher._isSuppressed = true; + } - public Suppressor(ScopedNotificationPublisher scopedNotificationPublisher) - { - _scopedNotificationPublisher = scopedNotificationPublisher; - _scopedNotificationPublisher._isSuppressed = true; - } + public void Dispose() => Dispose(true); - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - if (!_disposedValue) + if (disposing) { - if (disposing) + lock (_scopedNotificationPublisher._locker) { - lock (_scopedNotificationPublisher._locker) - { - _scopedNotificationPublisher._isSuppressed = false; - } + _scopedNotificationPublisher._isSuppressed = false; } - _disposedValue = true; } + + _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 c1e626c6c127..2e75d1b58391 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 +namespace Umbraco.Cms.Core.Events; + +public class SendEmailEventArgs : EventArgs { - public class SendEmailEventArgs : EventArgs - { - public EmailMessage Message { get; } + public SendEmailEventArgs(EmailMessage message) => Message = message; - 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 9b4e07814941..a72cd8201293 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 d733f0706a88..21137968f0fb 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 2c8dde89a235..8495da25b001 100644 --- a/src/Umbraco.Core/Events/TransientEventMessagesFactory.cs +++ b/src/Umbraco.Core/Events/TransientEventMessagesFactory.cs @@ -1,18 +1,11 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// A simple/default transient messages factory +/// +public class TransientEventMessagesFactory : IEventMessagesFactory { - /// - /// A simple/default transient messages factory - /// - public class TransientEventMessagesFactory : IEventMessagesFactory - { - public EventMessages Get() - { - return new EventMessages(); - } + public EventMessages Get() => new EventMessages(); - public EventMessages? GetOrDefault() - { - return null; - } - } + public EventMessages? GetOrDefault() => null; } diff --git a/src/Umbraco.Core/Events/TypedEventHandler.cs b/src/Umbraco.Core/Events/TypedEventHandler.cs index 11301448e0d0..e359bd47f9e1 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 17946a781f4f..f3a77e22e6db 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 96425e644f30..042355630f70 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.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) + _notifier.Notify(_actions.GetAction(), entities!); + } + + 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()) { - var newEntities = new List(); - var updatedEntities = new List(); + _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 (var entity in notification.SavedEntities) + // 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); + } + else + { + // it's updating + updatedEntities.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; + _notifier.Notify(_actions.GetAction(), newEntities.ToArray()); + _notifier.Notify(_actions.GetAction(), updatedEntities.ToArray()); + } - var parent = _contentService.GetById(parentId[0]); - if (parent == null) - return; // this shouldn't happen + public void Handle(ContentSentToPublishNotification notification) => + _notifier.Notify(_actions.GetAction(), notification.Entity); - _notifier.Notify(_actions.GetAction(), new[] { parent }); + 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(ContentPublishedNotification notification) => _notifier.Notify(_actions.GetAction(), notification.PublishedEntities.ToArray()); + // 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(ContentMovedNotification notification) + IContent? parent = _contentService.GetById(parentId[0]); + if (parent == null) { - // 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()) - { - _notifier.Notify(_actions.GetAction(), restoredEntities); - } + return; // this shouldn't happen } - public void Handle(ContentMovedToRecycleBinNotification notification) => _notifier.Notify(_actions.GetAction(), notification.MoveInfoCollection.Select(m => m.Entity).ToArray()); + _notifier.Notify(_actions.GetAction(), parent); + } - public void Handle(ContentCopiedNotification notification) => _notifier.Notify(_actions.GetAction(), notification.Original); + public void Handle(ContentUnpublishedNotification notification) => + _notifier.Notify(_actions.GetAction(), notification.UnpublishedEntities.ToArray()); - public void Handle(ContentRolledBackNotification notification) => _notifier.Notify(_actions.GetAction(), notification.Entity); + public void Handle(PublicAccessEntrySavedNotification notification) + { + IContent[] entities = _contentService.GetByIds(notification.SavedEntities.Select(e => e.ProtectedNodeId)).ToArray(); + if (entities.Any() == false) + { + return; + } - public void Handle(ContentSentToPublishNotification notification) => _notifier.Notify(_actions.GetAction(), notification.Entity); + _notifier.Notify(_actions.GetAction(), entities); + } - public void Handle(ContentUnpublishedNotification notification) => _notifier.Notify(_actions.GetAction(), notification.UnpublishedEntities.ToArray()); + /// + /// 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; - - /// - /// Initializes a new instance of the class. - /// - public Notifier( - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IHostingEnvironment hostingEnvironment, - INotificationService notificationService, - IUserService userService, - ILocalizedTextService textService, - IOptionsMonitor globalSettings, - ILogger logger) - { - _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; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _hostingEnvironment = hostingEnvironment; + _notificationService = notificationService; + _userService = userService; + _textService = textService; + _globalSettings = globalSettings.CurrentValue; + _logger = logger; - //if there is no current user, then use the admin - 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; - } - } + globalSettings.OnChange(x => _globalSettings = x); + } - SendNotification(user, entities, action, _hostingEnvironment.ApplicationMainUrl); - } + public void Notify(IAction? action, params IContent[] entities) + { + IUser? user = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser; - private void SendNotification(IUser sender, IEnumerable entities, IAction? action, Uri? siteUri) + // if there is no current user, then use the admin + if (user == null) { - if (sender == null) - throw new ArgumentNullException(nameof(sender)); - if (siteUri == 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 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 fa2399fc5c77..fd55a94b7b56 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 eeac07869d1c..5ade44a68fe5 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 - { - /// - /// Defines the default boot failed exception message. - /// - public const string DefaultMessage = "Boot failed: Umbraco cannot run. See Umbraco's log file for more details."; + 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. + /// + 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(); + throw new BootFailedException(DefaultMessage); + } - Exception? e = bootFailedException; - var m = new StringBuilder(); - m.Append(DefaultMessage); - while (e != null) + // 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\n-> {e.GetType().FullName}: {e.Message}"); - if (string.IsNullOrWhiteSpace(e.StackTrace) == false) - m.Append($"\n{e.StackTrace}"); - e = e.InnerException; + m.Append($"\n{e.StackTrace}"); } - throw new BootFailedException(m.ToString()); + + 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 fe711a98231d..89d8bfc01d5f 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 0b56cfb72cd0..9acc6ded3890 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) - { - Operation = operation; - } + /// + /// 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); - /// - /// 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) + /// + /// 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) { - if (info == null) - { - throw new ArgumentNullException(nameof(info)); - } + throw new ArgumentNullException(nameof(info)); + } - info.AddValue(nameof(Operation), Operation is not null ? Enum.GetName(typeof(T), Operation) : string.Empty); + 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 ba8c2b61069c..9bc51d7b6e63 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. + /// + public InvalidCompositionException() + { + } + + /// + /// 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 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 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. + /// + /// 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) + { + 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[])); + } + + /// + /// 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. /// - /// - [Serializable] - public class InvalidCompositionException : 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) { - /// - /// 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; } - - /// - /// Initializes a new instance of the class. - /// - public InvalidCompositionException() - { } - - /// - /// 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 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 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)) + 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); } - - /// - /// 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) + else { - 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( + "Content type with alias '{0}' was added as a composition to content type with alias '{1}', but there was a conflict.", + addedCompositionAlias, + contentTypeAlias); } - /// - /// 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 (propertyTypeAliases.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); + sb.AppendFormat( + " Property types must have a unique alias across all compositions, these aliases are duplicate: {0}.", + string.Join(", ", propertyTypeAliases)); + } - base.GetObjectData(info, context); + 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(); } } diff --git a/src/Umbraco.Core/Exceptions/PanicException.cs b/src/Umbraco.Core/Exceptions/PanicException.cs index 9ba1311e84ec..99ce96c27301 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 2a2b97b23d3e..f65da5074522 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 1895364d1722..79e03d7b9395 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(); - - /// - /// 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); - } + 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 property lambda. - /// - /// - public static PropertyInfo GetPropertyInfo(Expression> propertyLambda) - { - return PropertyInfoCache.GetOrAdd( - new LambdaExpressionCacheKey(propertyLambda), - x => - { - var type = typeof(TSource); + /// + /// 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 => + { + Type type = typeof(TSource); - var member = propertyLambda.Body as MemberExpression; - if (member == null) + 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) + if (propertyLambda.Body is UnaryExpression unary) { - if (propertyLambda.Body.GetType().Name == "UnaryExpression") + if (unary.Operand is not MemberExpression boxedMember) { - // 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; - } + 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 throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.", propertyLambda)); + + 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)); + } - 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)); + } - 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; + }); - 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 (MemberInfo, string?) FindProperty(LambdaExpression lambda) + Expression expr = lambda; + var loop = true; + string? alias = null; + while (loop) { - void Throw() + switch (expr.NodeType) { - 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)); - } + case ExpressionType.Convert: + 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(); + } - Expression expr = lambda; - var loop = true; - string? alias = null; - while (loop) - { - switch (expr.NodeType) - { - 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; - } + 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; } + } + + throw new Exception("Configuration for members is only supported for top-level individual members on a type."); + } - 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) + { + return null; } - public static IDictionary? GetMethodParams(Expression> fromExpression) + if (fromExpression.Body is not MethodCallExpression body) { - 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; + return new Dictionary(); } - /// - /// 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) + 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) { - if (fromExpression == null) return null; - var body = fromExpression.Body as MethodCallExpression; - return body != null ? body.Method : null; + LambdaExpression lambda = Expression.Lambda(argument, fromExpression.Parameters); + Delegate d = lambda.Compile(); + var value = d.DynamicInvoke(new object[1]); + rVal.Add(parameters[i]!, value); + i++; } - /// - /// Gets the method info. - /// - /// The return type of the method. - /// From expression. - /// - public static MethodInfo? GetMethodInfo(Expression> fromExpression) + 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) { - if (fromExpression == null) return null; - var body = fromExpression.Body as MethodCallExpression; - return body != null ? body.Method : null; + return null; } - /// - /// Gets the method info. - /// - /// The type of the 1. - /// The type of the 2. - /// From expression. - /// - public static MethodInfo? GetMethodInfo(Expression> fromExpression) + 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) { - if (fromExpression == null) return null; + return null; + } - MethodCallExpression? me; - switch (fromExpression.Body.NodeType) - { - case ExpressionType.Convert: - case ExpressionType.ConvertChecked: - var ue = fromExpression.Body as UnaryExpression; - me = ((ue != null) ? ue.Operand : null) as MethodCallExpression; - break; - default: - me = fromExpression.Body as MethodCallExpression; - break; - } + var body = fromExpression.Body as MethodCallExpression; + return body?.Method; + } - return me != null ? me.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; } - /// - /// 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) + MethodCallExpression? me; + switch (fromExpression.Body.NodeType) { - if (expression == null) return null; - return IsMethod(expression) ? (((MethodCallExpression)expression).Method) : null; + 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; } - /// - /// 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) + 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) { - if (fromExpression == null) return 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 IsMethod(expression) ? ((MethodCallExpression)expression).Method : null; + } - return me != null ? me.Member : 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; } - /// - /// 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) + 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) + { + return false; + } + + if (left.Name != right.Name) { - 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) + ParameterInfo[] leftParams = left.GetParameters(); + ParameterInfo[] rightParams = right.GetParameters(); + if (leftParams.Length != rightParams.Length) { - if (fromMethodGroup == null) throw new ArgumentNullException("fromMethodGroup"); + return false; + } + for (var 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; + } - return fromMethodGroup.Method; + // if they are not delegates, then compare the types + if (leftParams[i].ParameterType != rightParams[i].ParameterType) + { + 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) + if (left.ReturnType != right.ReturnType) { - return expression is MethodCallExpression; + return false; } + return true; + } - /// - /// 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) + /// + /// Gets a from an provided it refers to member access. + /// + /// The expression. + /// + /// + public static MemberInfo? GetMember(Expression expression) + { + if (expression == null) { - return expression is MemberExpression; + return null; } - /// - /// 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) + return IsMember(expression) ? ((MemberExpression)expression).Member : null; + } + + /// + /// Gets a from a + /// + /// From method group. + /// + /// + public static MethodInfo GetStaticMethodInfo(Delegate fromMethodGroup) + { + if (fromMethodGroup == null) { - return expression is ConstantExpression; + throw new ArgumentNullException("fromMethodGroup"); } - /// - /// 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 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) { - if (arguments == null) return false; - return - arguments.Where(x => x is ConstantExpression).Cast - ().Select(x => x.Value).DefaultIfEmpty(null).FirstOrDefault(); + 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 aea0f847abb6..45ae9ceafeb1 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 = ""; + 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) + /// + /// 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)) { - var codeBase = assembly.Location; - var uri = new Uri(codeBase); - var path = uri.LocalPath; - return new FileInfo(path); + throw new Exception( + "No root directory could be resolved. Please ensure that your Umbraco solution is correctly configured."); } - /// - /// Returns true if the assembly is the App_Code assembly - /// - /// - /// - public static bool IsAppCodeAssembly(this Assembly assembly) + _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")) { - if (assembly.FullName!.StartsWith("App_Code")) + try + { + Assembly.Load("App_Code"); + return true; + } + catch (FileNotFoundException) { - try - { - Assembly.Load("App_Code"); - return true; - } - catch (FileNotFoundException) - { - //this will occur if it cannot load the assembly - return false; - } + // 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"); - } + return false; + } - /// - /// 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); - } + /// + /// Returns true if the assembly is the compiled global asax. + /// + /// + /// + public static bool IsGlobalAsaxAssembly(this Assembly assembly) => - return null; + // 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 uri = new Uri(codeBase); + var path = uri.LocalPath; + return new FileInfo(path); } + return null; } } diff --git a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs index e3d6f7f4fd33..a604b3e0173e 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[] + { + 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) { - 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)); + userId = claimsIdentity.FindFirstValue(ClaimTypes.NameIdentifier) + ?? claimsIdentity.FindFirstValue("sub"); + } - string? userId = null; - if (identity is ClaimsIdentity claimsIdentity) - { - userId = claimsIdentity.FindFirstValue(ClaimTypes.NameIdentifier) - ?? claimsIdentity.FindFirstValue("sub"); - } + return userId; + } - 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) + { + throw new ArgumentNullException(nameof(identity)); } - /// - /// 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) + string? username = null; + if (identity is ClaimsIdentity claimsIdentity) { - if (identity == null) throw new ArgumentNullException(nameof(identity)); + username = claimsIdentity.FindFirstValue(ClaimTypes.Name) + ?? claimsIdentity.FindFirstValue("preferred_username"); + } - string? username = null; - if (identity is ClaimsIdentity claimsIdentity) - { - username = claimsIdentity.FindFirstValue(ClaimTypes.Name) - ?? claimsIdentity.FindFirstValue("preferred_username"); - } + return username; + } - return username; + public static string? GetEmail(this IIdentity identity) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); } - public static string? GetEmail(this IIdentity identity) + string? email = null; + if (identity is ClaimsIdentity claimsIdentity) { - if (identity == null) throw new ArgumentNullException(nameof(identity)); + email = claimsIdentity.FindFirstValue(ClaimTypes.Email); + } - string? email = null; - if (identity is ClaimsIdentity claimsIdentity) - { - email = claimsIdentity.FindFirstValue(ClaimTypes.Email); - } + return email; + } - 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) + { + throw new ArgumentNullException(nameof(identity)); } - /// - /// 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 identity.FindFirst(claimType)?.Value; + } - 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; } - /// - /// 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[] - { - 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 - }; - - /// - /// 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) + // Validate that all required claims exist + foreach (var claimType in RequiredBackOfficeClaimTypes) { - if (identity is null) + 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; + } - 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) + { + identity.AddClaim(new Claim( + ClaimTypes.NameIdentifier, + userId, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); } - /// - /// 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) + if (identity.HasClaim(x => x.Type == ClaimTypes.Name) == 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)); - } + 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 == ClaimTypes.Name) == false) + if (identity.HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false && + startContentNodes != null) + { + foreach (var startContentNode in startContentNodes) { identity.AddClaim(new Claim( - ClaimTypes.Name, - username, - ClaimValueTypes.String, + Constants.Security.StartContentNodeIdClaimType, + startContentNode.ToInvariantString(), + ClaimValueTypes.Integer32, Constants.Security.BackOfficeAuthenticationType, Constants.Security.BackOfficeAuthenticationType, identity)); } + } - if (identity.HasClaim(x => x.Type == ClaimTypes.GivenName) == false) + if (identity.HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false && + startMediaNodes != null) + { + foreach (var startMediaNode in startMediaNodes) { identity.AddClaim(new Claim( - ClaimTypes.GivenName, - realName, - ClaimValueTypes.String, + Constants.Security.StartMediaNodeIdClaimType, + startMediaNode.ToInvariantString(), + ClaimValueTypes.Integer32, 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 == ClaimTypes.Locality) == false) + { + identity.AddClaim(new Claim( + ClaimTypes.Locality, + culture, + ClaimValueTypes.String, + 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)); - } - } + // 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)); + } - if (identity.HasClaim(x => x.Type == ClaimTypes.Locality) == false) + // 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( - ClaimTypes.Locality, - culture, + Constants.Security.AllowedApplicationsClaimType, + application, 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) + // 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( - Constants.Security.SecurityStampClaimType, - securityStamp, + identity.RoleClaimType, + roleName, 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) + /// + /// 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) { - var firstValue = identity.FindFirstValue(ClaimTypes.NameIdentifier); - if (firstValue is not null) - { - return int.Parse(firstValue, CultureInfo.InvariantCulture); - } - - return null; + return int.Parse(firstValue, CultureInfo.InvariantCulture); } - /// - /// 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) + 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) { - if (identity == null) - { - throw new ArgumentNullException(nameof(identity)); - } + throw new ArgumentNullException(nameof(identity)); + } - if (claim is not null) - { - Claim? existingClaim = identity.Claims.FirstOrDefault(x => x.Type == claim.Type); - identity.TryRemoveClaim(existingClaim); + if (claim is not null) + { + Claim? existingClaim = identity.Claims.FirstOrDefault(x => x.Type == claim.Type); + identity.TryRemoveClaim(existingClaim); - identity.AddClaim(claim); - } + identity.AddClaim(claim); } } } diff --git a/src/Umbraco.Core/Extensions/ContentExtensions.cs b/src/Umbraco.Core/Extensions/ContentExtensions.cs index 0bd1e36d9eb0..df0e58d8781f 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.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; - } + mediaFilePath = null; + return false; + } - if (!mediaUrlGenerators.TryGetMediaPath( + 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()); + return false; } - public static bool WasAnyUserPropertyDirty(this IContentBase entity) - { - return entity.Properties.Any(x => x.WasDirty()); - } + return true; + } + public static bool IsAnyUserPropertyDirty(this IContentBase entity) => entity.Properties.Any(x => x.IsDirty()); - 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; - } + public static bool WasAnyUserPropertyDirty(this IContentBase entity) => 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) + /// + /// 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) { - entity.Name = entity.Name?.ToValidXmlString(); - foreach (var property in entity.Properties) + foreach (IPropertyValue propertyValue in property.Values) { - foreach (var propertyValue in property.Values) + if (propertyValue.EditedValue is string editString) { - if (propertyValue.EditedValue is string editString) - propertyValue.EditedValue = editString.ToValidXmlString(); - if (propertyValue.PublishedValue is string publishedString) - propertyValue.PublishedValue = publishedString.ToValidXmlString(); + 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) + /// + /// Returns all properties based on the editorAlias + /// + /// + /// + /// + public static IEnumerable GetPropertiesByEditor(this IContentBase content, string editorAlias) + => content.Properties.Where(x => x.PropertyType?.PropertyEditorAlias == editorAlias); + + /// + /// 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) { - if (content is IContent) - { - return services.ContentService?.HasChildren(content.Id); - } - if (content is IMedia) - { - return services.MediaService?.HasChildren(content.Id); - } - return false; + return services.ContentService?.HasChildren(content.Id); } - - /// - /// 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 (content is IMedia) { - 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 services.MediaService?.HasChildren(content.Id); } - /// - /// 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 - + return false; + } - /// - /// Gets the for the Creator of this content item. - /// - public static IProfile? GetCreatorProfile(this IContentBase content, IUserService userService) - { - return userService.GetProfileById(content.CreatorId); - } - /// - /// Gets the for the Writer of this content. - /// - public static IProfile? GetWriterProfile(this IContent content, IUserService userService) + /// + /// 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 userService.GetProfileById(content.WriterId); + return ContentStatus.Trashed; } - /// - /// Gets the for the Writer of this content. - /// - public static IProfile? GetWriterProfile(this IMedia content, IUserService userService) + if (!content.ContentType.VariesByCulture()) { - return userService.GetProfileById(content.WriterId); + culture = string.Empty; } - - - #region User/Profile methods - - /// - /// Gets the for the Creator of this media item. - /// - public static IProfile? GetCreatorProfile(this IMedia media, IUserService userService) + else if (culture.IsNullOrWhiteSpace()) { - return userService.GetProfileById(media.CreatorId); + throw new ArgumentNullException($"{nameof(culture)} cannot be null or empty"); } - - #endregion - - - /// - /// Returns properties that do not belong to a group - /// - /// - /// - public static IEnumerable GetNonGroupedProperties(this IContentBase content) + IEnumerable expires = contentSchedule.GetSchedule(culture!, ContentScheduleAction.Expire); + if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date)) { - return content.Properties - .Where(x => x.PropertyType?.PropertyGroupId == null) - .OrderBy(x => x.PropertyType?.SortOrder); + return ContentStatus.Expired; } - /// - /// Returns the Property object for the given property group - /// - /// - /// - /// - public static IEnumerable GetPropertiesForGroup(this IContentBase content, PropertyGroup propertyGroup) + IEnumerable release = contentSchedule.GetSchedule(culture!, ContentScheduleAction.Release); + if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now)) { - //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)); + return ContentStatus.AwaitingRelease; } - - #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 (content.Published) { - 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); + return ContentStatus.Published; } - 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)) - { - 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); - } + return ContentStatus.Unpublished; + } - // gets or creates a property for a content item. - private static IProperty GetProperty(IContentBase content, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias) + /// + /// 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) { - var property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); - if (property != null) - return property; - - 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 + "."); - - property = new Property(propertyType); - content.Properties.Add(property); - return property; + return; } - /// - /// 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) + filename = shortStringHelper.CleanStringForSafeFileName(filename); + if (string.IsNullOrWhiteSpace(filename)) { - 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); + return; } - #endregion - + filename = filename.ToLower(); - #region Dirty + SetUploadFile(content, mediaFileManager, mediaUrlGenerators, contentTypeBaseServiceProvider, propertyTypeAlias, filename, filestream, culture, segment); + } - public static IEnumerable GetDirtyUserProperties(this IContentBase entity) + /// + /// 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) { - return entity.Properties.Where(x => x.IsDirty()).Select(x => x.Alias); + 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); - #endregion - + // 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; - /// - /// 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) + if (content.TryGetMediaPath(property.Alias, mediaUrlGenerators, out var mediaFilePath, culture, segment)) { - return serializer.Serialize(content, false, true); + oldpath = mediaFileManager.FileSystem.GetRelativePath(mediaFilePath!); } - /// - /// 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); - } + 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); + } - /// - /// 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) + // 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 serializer.Serialize(media); + return property; } - /// - /// 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) + IContentTypeComposition? contentType = contentTypeBaseServiceProvider.GetContentTypeOf(content); + IPropertyType? propertyType = contentType?.CompositionPropertyTypes + .FirstOrDefault(x => x.Alias?.InvariantEquals(propertyTypeAlias) ?? false); + if (propertyType == null) { - return serializer.Serialize(member); + throw new Exception("No property type exists with alias " + propertyTypeAlias + "."); } + + 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 4469683acb1b..2654bf0e1e06 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. + /// + /// 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. /// - public static class ContentVariationExtensions + /// 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(); - - /// - /// 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) - { - return value - ? variations | variation // Set flag using bitwise logical OR - : variations & ~variation; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) - } + culture = culture?.NullOrWhiteSpaceAsNull(); + segment = segment?.NullOrWhiteSpaceAsNull(); - /// - /// 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 wildcards are disabled, do not allow "*" + if (!wildcards && (culture == "*" || segment == "*")) { - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); - - // if wildcards are disabled, do not allow "*" - if (!wildcards && (culture == "*" || segment == "*")) + if (throwIfInvalid) { - if (throwIfInvalid) - throw new NotSupportedException($"Variation wildcards are not supported."); - return false; + throw new NotSupportedException("Variation wildcards are not supported."); } - if (variation.VariesByCulture()) + return false; + } + + if (variation.VariesByCulture()) + { + // varies by culture + // in exact mode, the culture cannot be null + if (exact && culture == null) { - // varies by culture - // in exact mode, the culture cannot be null - if (exact && culture == null) + if (throwIfInvalid) { - 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."); } + + return false; } - else + } + else + { + // does not vary by culture + // the culture cannot have a value + // unless wildcards and it's "*" + if (culture != null && !(wildcards && culture == "*")) { - // does not vary by culture - // the culture cannot have a value - // unless wildcards and it's "*" - if (culture != null && !(wildcards && culture == "*")) + if (throwIfInvalid) { - if (throwIfInvalid) - throw new NotSupportedException($"Culture \"{culture}\" is invalid because culture variation is disabled."); - - return false; + 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 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) { - if (throwIfInvalid) - throw new NotSupportedException($"Segment \"{segment}\" is invalid because segment variation is disabled."); - - return false; + throw new NotSupportedException( + $"Segment \"{segment}\" is invalid because segment variation is disabled."); } - return true; + return false; } + + return true; } } diff --git a/src/Umbraco.Core/Extensions/CoreCacheHelperExtensions.cs b/src/Umbraco.Core/Extensions/CoreCacheHelperExtensions.cs index 8dfec45c7ecc..1af0b8c47a21 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 +namespace Umbraco.Extensions; + +/// +/// Extension methods for the cache helper +/// +public static class CoreCacheHelperExtensions { + public const string PartialViewCacheKey = "Umbraco.Web.PartialViewCacheKey"; + /// - /// Extension methods for the cache helper + /// Clears the cache for partial views /// - 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); - } - } + /// + 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 4594709407c5..10fa51deaff4 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 - var tableData = rowData().ToArray(); + var dt = new DataTable(tableAlias); - //get all headers - var propertyHeaders = GetPropertyHeaders(tableAlias, getHeaders); - foreach(var h in propertyHeaders) - { - dt.Columns.Add(new DataColumn(h.Value)); - } + // get all row data + Tuple>, IEnumerable>>[] tableData = + rowData().ToArray(); - //add row data - foreach(var r in tableData) - { - dt.PopulateRow( - propertyHeaders, - r.Item1, - r.Item2); - } - - 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() + // get all headers + IDictionary propertyHeaders = GetPropertyHeaders(tableAlias, getHeaders); + foreach (KeyValuePair h in propertyHeaders) { - return new List>, IEnumerable>>>(); + dt.Columns.Add(new DataColumn(h.Value)); } - /// - /// 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) + // add row data + foreach (Tuple>, IEnumerable>> r in + tableData) { - rowData.Add(new System.Tuple>, IEnumerable>>( - standardVals, - userVals - )); + dt.PopulateRow( + propertyHeaders, + r.Item1, + r.Item2); } - private static IDictionary GetPropertyHeaders(string alias, Func>> getHeaders) + 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) { - var headers = getHeaders(alias); - var def = headers.ToDictionary(pt => pt.Key, pt => pt.Value); - return def; + dr[r.Key] = r.Value; } - private static void PopulateRow( - this DataTable dt, - IDictionary aliasesToNames, - IEnumerable> standardVals, - IEnumerable> userPropertyVals) + foreach (KeyValuePair p in userPropertyVals.Where(p => p.Value != null)) { - 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 e500cf86b039..35c9f600e558 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 + { + 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) { - /// - /// Returns the DateTime as an ISO formatted string that is globally expectable - /// - /// - /// - public static string ToIsoString(this DateTime dt) + 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 dt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + return new DateTime(dt.Year, dt.Month, dt.Day); } - public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo) + if (truncateTo == DateTruncate.Hour) { - 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, dt.Day, dt.Hour, 0, 0); } - public enum DateTruncate + if (truncateTo == DateTruncate.Minute) { - Year, - Month, - Day, - Hour, - Minute, - Second + 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 fa6280584168..6e70544d0ee7 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 4cbcdd5d6a94..621ef46438ff 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 + + 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) { - attempts++; - var result = task(attempts); - if (result.Success) { return result; } - Thread.Sleep((int)pause.TotalMilliseconds); + return result; } - while (attempts < totalAttempts); - return Attempt.Fail(); + + 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 906f12282e8e..3bbd3bdcb996 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 +namespace Umbraco.Extensions; + +/// +/// Extension methods for Dictionary & ConcurrentDictionary +/// +public static class DictionaryExtensions { - /// - /// 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() { - - /// - /// 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; } - return false; + + // if we're looping either the key was removed by another thread, or another thread + // changed the value, so we start again. } - /// - /// 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 + 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)) { - 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. + return false; } - /// - /// Converts a dictionary to another type by only using direct casting - /// - /// - /// - /// - /// - public static IDictionary ConvertTo(this IDictionary d) - where TKeyOut : 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) { - var result = new Dictionary(); - foreach (DictionaryEntry v in d) - { - result.Add((TKeyOut)v.Key, (TValOut)v.Value!); - } - return result; + result.Add((TKeyOut)v.Key, (TValOut)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 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(keyConverter(v.Key), valConverter(v.Value!)); - } - return result; + result.Add(keyConverter(v.Key), valConverter(v.Value!)); } - /// - /// Converts a dictionary to a NameValueCollection - /// - /// - /// - public static NameValueCollection ToNameValueCollection(this IDictionary d) + 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 n = new NameValueCollection(); - foreach (var i in d) - { - n.Add(i.Key, i.Value); - } - return n; + 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 + /// + /// 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)) { - foreach (var p in sources.SelectMany(src => src).Where(p => overwrite || destination.ContainsKey(p.Key) == false)) - { - destination[p.Key] = p.Value; - } + 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); - /// - /// 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 + /// + /// 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)) { - destination.MergeLeft(new[] {source}, overwrite); + return d[key]; } - /// - /// 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)) + 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)) { - if (d.ContainsKey(key)) + var value = d[key]!.ToString(); + if (value != string.Empty) { - return d[key]; + return value; } - 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; + } - 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); - /// contains key ignore case. - /// The dictionary. - /// The key. - /// Value Type - /// The contains key ignore case. - public static bool ContainsKeyIgnoreCase(this IDictionary dictionary, string 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 dictionary.Keys.InvariantContains(key); + return string.Empty; } - /// - /// Converts a dictionary object to a query string representation such as: - /// firstname=shannon&lastname=deminick - /// - /// - /// - public static string ToQueryString(this IDictionary d) + var builder = new StringBuilder(); + foreach (KeyValuePair i in 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); + builder.Append(string.Format("{0}={1}&", WebUtility.UrlEncode(i.Key), i.Value == null ? string.Empty : WebUtility.UrlEncode(i.Value.ToString()))); } - /// 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 builder.ToString().TrimEnd(Constants.CharArrays.Ampersand); + } - return key.IsNullOrWhiteSpace() == false - ? dictionary[key!] - : defaultValue; - } + /// 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); - public static async Task> ToDictionaryAsync( - this IEnumerable enumerable, - Func syncKeySelector, - Func> asyncValueSelector) - where TKey : notnull - { - Dictionary dictionary = new Dictionary(); + /// 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)); - foreach (var item in enumerable) - { - var key = syncKeySelector(item); + 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(); - var value = await asyncValueSelector(item); + foreach (TInput item in enumerable) + { + TKey key = syncKeySelector(item); - dictionary.Add(key,value); - } + TValue value = await asyncValueSelector(item); - return dictionary; + dictionary.Add(key, value); } + + return dictionary; } } diff --git a/src/Umbraco.Core/Extensions/EnumExtensions.cs b/src/Umbraco.Core/Extensions/EnumExtensions.cs index e13467ef3251..3aa124d2f379 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 - { - /// - /// 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); - } + /// 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 - { - 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 28f4844f00a0..6628dc4f3ddb 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 true; } - return false; } + 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) + public static IEnumerable> InGroupsOf(this IEnumerable? source, int groupSize) + { + if (source == null) { - // see EnumeratorBenchmarks - this is faster, and allocates less, than returning an array - yield return item; + throw new ArgumentNullException("source"); } - public static IEnumerable> InGroupsOf(this IEnumerable? source, int groupSize) + if (groupSize <= 0) { - if (source == null) - throw new ArgumentNullException("source"); - if (groupSize <= 0) - throw new ArgumentException("Must be greater than zero.", "groupSize"); - + throw new ArgumentException("Must be greater than zero.", "groupSize"); + } - // following code derived from MoreLinq and does not allocate bazillions of tuples + // following code derived from MoreLinq and does not allocate bazillions of tuples + T[]? temp = null; + var count = 0; - T[]? temp = null; - var count = 0; + foreach (T item in source) + { + if (temp == null) + { + temp = new T[groupSize]; + } - foreach (var item in source) + temp[count++] = item; + if (count != groupSize) { - if (temp == null) temp = new T[groupSize]; - temp[count++] = item; - if (count != groupSize) continue; - yield return temp/*.Select(x => x)*/; - temp = null; - count = 0; + continue; } - if (temp != null && count > 0) - yield return temp.Take(count); + yield return temp /*.Select(x => x)*/; + temp = null; + count = 0; } - public static IEnumerable SelectByGroups(this IEnumerable source, Func, IEnumerable> selector, int groupSize) + if (temp != null && count > 0) { - // 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; + yield return temp.Take(count); } + } - /// - /// Returns a sequence of length whose elements are the result of invoking . - /// - /// - /// The factory. - /// The count. - /// - public static IEnumerable Range(Func factory, int 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)) { - for (int i = 1; i <= count; i++) + foreach (TResult result in resultGroup) { - yield return factory.Invoke(i - 1); + yield return result; } } + } - /// The if not null. - /// The items. - /// The action. - /// The type - public static void IfNotNull(this IEnumerable items, Action action) where TItem : class + /// + /// 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++) { - if (items != null) + 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) { - foreach (TItem item in items) - { - item.IfNotNull(action); - } + 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) + /// + /// 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) { - if (source == null) throw new ArgumentNullException("source"); - if (other == null) throw new ArgumentNullException("other"); - - return other.Except(source).Any() == false; + throw new ArgumentNullException("source"); } - /// - /// Returns true if the source contains any of the items in the other list - /// - /// - /// - /// - /// - public static bool ContainsAny(this IEnumerable source, IEnumerable other) + if (other == null) { - return other.Any(source.Contains); + throw new ArgumentNullException("other"); } - /// - /// Removes all matching items from an . - /// - /// - /// The list. - /// The predicate. - /// - public static void RemoveAll(this IList list, Func predicate) + 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++) { - for (var i = 0; i < list.Count; i++) + if (predicate(list[i])) { - if (predicate(list[i])) - { - list.RemoveAt(i--); - } + list.RemoveAt(i--); } } + } - /// - /// Removes all matching items from an . - /// - /// - /// The list. - /// The predicate. - /// - public static void RemoveAll(this ICollection list, Func predicate) + /// + /// 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) { - var matches = list.Where(predicate).ToArray(); - foreach (var match in matches) - { - list.Remove(match); - } + list.Remove(match); } + } - public static IEnumerable SelectRecursive( - this IEnumerable source, - Func> recursiveSelector, int maxRecusionDepth = 100) - { - var stack = new Stack>(); - stack.Push(source.GetEnumerator()); + public static IEnumerable SelectRecursive( + this IEnumerable source, + Func> recursiveSelector, + int maxRecusionDepth = 100) + { + var stack = new Stack>(); + stack.Push(source.GetEnumerator()); - try + try + { + while (stack.Count > 0) { - while (stack.Count > 0) + if (stack.Count > maxRecusionDepth) { - if (stack.Count > maxRecusionDepth) - throw new InvalidOperationException("Maximum recursion depth reached of " + maxRecusionDepth); + throw new InvalidOperationException("Maximum recursion depth reached of " + maxRecusionDepth); + } - if (stack.Peek().MoveNext()) - { - var current = stack.Peek().Current; + if (stack.Peek().MoveNext()) + { + TSource current = stack.Peek().Current; - yield return current; + yield return current; - stack.Push(recursiveSelector(current).GetEnumerator()); - } - else - { - stack.Pop().Dispose(); - } + stack.Push(recursiveSelector(current).GetEnumerator()); } - } - 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)!; + while (stack.Count > 0) + { + stack.Pop().Dispose(); + } } + } - public static IEnumerable ForAllThatAre(this IEnumerable sequence, Action projection) - where TActual : class - { - return sequence.Select( - x => + /// + /// 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) { - if (x is TActual casted) - { - projection.Invoke(casted); - } - return x; - }); + 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"); } - /// - /// 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) + if (predicate == null) { - return FindIndex(items, 0, predicate); + throw new ArgumentNullException("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 (startIndex < 0) { - if (items == null) throw new ArgumentNullException("items"); - if (predicate == null) throw new ArgumentNullException("predicate"); - if (startIndex < 0) throw new ArgumentOutOfRangeException("startIndex"); + throw new ArgumentOutOfRangeException("startIndex"); + } - var index = startIndex; - if (index > 0) - items = items.Skip(index); + var index = startIndex; + if (index > 0) + { + items = items.Skip(index); + } - foreach (var item in items) + foreach (T item in items) + { + if (predicate(item)) { - if (predicate(item)) return index; - index++; + return index; } - return -1; + index++; } - ///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 -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 items.FindIndex(i => EqualityComparer.Default.Equals(item, i)); + return true; } - /// - /// 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) { - if (source == null && other == null) return true; - if (source == null || other == null) return false; + return false; + } - var list1Groups = source.ToLookup(i => i); - var list2Groups = other.ToLookup(i => i); - return list1Groups.Count == list2Groups.Count + 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) - { - return transform(source); - } + /// + /// 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) - { - return items ?? Enumerable.Empty(); - } + /// + /// 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) - { - return contents.Where(x => types.Contains(x?.GetType())); - } + // 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) + public static IEnumerable SkipLast(this IEnumerable source) + { + using (IEnumerator e = source.GetEnumerator()) { - using (var e = source.GetEnumerator()) + if (e.MoveNext() == false) { - if (e.MoveNext() == false) yield break; - - for (var value = e.Current; e.MoveNext(); value = e.Current) - yield return value; + yield break; } - } - public static IOrderedEnumerable OrderBy(this IEnumerable source, Func keySelector, Direction sortOrder) - { - return sortOrder == Direction.Ascending ? source.OrderBy(keySelector) : source.OrderByDescending(keySelector); + 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 d76f39a8de97..12476c95068c 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() { return f => true; } + public static Expression> True() => f => true; - public static Expression> False() { return f => false; } + public static Expression> False() => 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> Or(this Expression> left, Expression> right) + { + InvocationExpression 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 944c9360b4a1..f1b19569ff93 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; - - /// - /// 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 root = hostEnvironment.ContentRootPath; + var root = hostEnvironment.ContentRootPath; - var newPath = path.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); + 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)); + // 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"); } - /// - /// 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 b4ef60ea57cd..1ab908b44526 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) + /// The if true. + /// The predicate. + /// The action. + public static void IfTrue(this bool predicate, Action action) + { + if (predicate) { - if (predicate) - { - action(); - } + 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 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!); - } + /// + /// 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 4f79baa3f505..d347993dd07e 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); - } + 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); - } + /// + /// 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 73927f7a4123..7189c4cc15ef 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 2c4627196462..f9aec08a6194 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 a07abfbd9693..f8fdcdc83f7f 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]); - } + yield return new KeyValuePair(key, nvc[key]); } + } - public static bool ContainsKey(this NameValueCollection collection, string 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 collection.Keys.Cast().Any(k => (string) k == key); + return defaultIfNotFound; } - public static T? GetValue(this NameValueCollection collection, string key, T defaultIfNotFound) + var val = collection[key]; + if (val == null) { - if (collection.ContainsKey(key) == false) - { - return defaultIfNotFound; - } - - var val = collection[key]; - if (val == null) - { - return defaultIfNotFound; - } + return defaultIfNotFound; + } - var result = val.TryConvertTo(); + Attempt result = val.TryConvertTo(); - return result.Success ? result.Result : defaultIfNotFound; - } + return result.Success ? result.Result : defaultIfNotFound; } } diff --git a/src/Umbraco.Core/Extensions/ObjectExtensions.cs b/src/Umbraco.Core/Extensions/ObjectExtensions.cs index 1ba7e0fc4d6c..6dc220446baa 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 Umbraco.Cms.Core; using Umbraco.Cms.Core.Collections; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides object extension methods. +/// +public static class ObjectExtensions { - /// - /// Provides object extension methods. - /// - public static class ObjectExtensions - { - 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 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 CustomBooleanTypeConverter(); + private static readonly char[] NumberDecimalSeparatorsToNormalize = { '.', ',' }; + private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new(); - //private static readonly ConcurrentDictionary> ObjectFactoryCache = new ConcurrentDictionary>(); + // private static readonly ConcurrentDictionary> ObjectFactoryCache = new ConcurrentDictionary>(); - /// - /// - /// - /// - /// - /// - public static IEnumerable AsEnumerableOfOne(this T input) + /// + /// + /// + /// + /// + public static IEnumerable AsEnumerableOfOne(this T input) => Enumerable.Repeat(input, 1); + + /// + /// + /// + public static void DisposeIfDisposable(this object 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(); + return default; } - /// - /// 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 (input is T variable) { - if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) return default; - if (input is T variable) return variable; - return default; + return variable; } - /// - /// 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) + 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) { - Attempt result = TryConvertTo(input, typeof(T)); + return Attempt.Succeed((T?)result.Result); + } - if (result.Success) + if (input == null) + { + if (typeof(T).IsValueType) { - return Attempt.Succeed((T?)result.Result); + // fail, cannot convert null to a value type + return Attempt.Fail(); } + // 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 (typeof(T).IsValueType) - { - // fail, cannot convert null to a value type - return Attempt.Fail(); - } - else + // Nullable is ok + if (target.IsGenericType && GetCachedGenericNullableType(target) != null) { - // sure, null can be any object - return Attempt.Succeed((T)input!); + return Attempt.Succeed(null); } - } - // just try to cast - try - { - return Attempt.Succeed((T)input); + // Reference types are ok + return Attempt.If(target.IsValueType == false, null); } - catch (Exception e) + + Type inputType = input.GetType(); + + // Easy + if (target == typeof(object) || inputType == target) { - return Attempt.Fail(e); + return Attempt.Succeed(input); } - } - /// - /// 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) + // Check for string so that overloaders of ToString() can take advantage of the conversion. + if (target == typeof(string)) { - return Attempt.Fail(); + return Attempt.Succeed(input.ToString()); } - try + // 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) { - 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) + Type? underlying = GetCachedGenericNullableType(target); + if (underlying != null) { - 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) { - // 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) + // 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))) { - input = inner.Result; // Now fall on through... - } - else - { - return Attempt.Fail(inner.Exception); + return Attempt.Succeed(null); } } - } - else - { - // target is not a generic type - if (input is string inputString) + // 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) { - // 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; - } + input = inner.Result; // Now fall on through... } - - // 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)) + else { - return Attempt.Succeed(Convert.ChangeType(input, target)); + return Attempt.Fail(inner.Exception); } } - - if (target == typeof(bool)) + } + else + { + // target is not a generic type + if (input is string inputString) { - if (GetCachedCanConvertToBoolean(inputType)) + // 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 Attempt.Succeed(CustomBooleanTypeConverter.ConvertFrom(input!)); + return result.Value; } } - var inputConverter = GetCachedSourceTypeConverter(inputType, target); - if (inputConverter != null) + // 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(inputConverter.ConvertTo(input, target)); + return Attempt.Succeed(Convert.ChangeType(input, target)); } + } - var outputConverter = GetCachedTargetTypeConverter(inputType, target); - if (outputConverter != null) + if (target == typeof(bool)) + { + if (GetCachedCanConvertToBoolean(inputType)) { - return Attempt.Succeed(outputConverter.ConvertFrom(input!)); + return Attempt.Succeed(CustomBooleanTypeConverter.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); - } + TypeConverter? inputConverter = GetCachedSourceTypeConverter(inputType, target); + if (inputConverter != null) + { + return Attempt.Succeed(inputConverter.ConvertTo(input, target)); + } - // Re-check convertibles since we altered the input through recursion - if (input is IConvertible convertible2) - { - return Attempt.Succeed(Convert.ChangeType(convertible2, target)); - } + TypeConverter? outputConverter = GetCachedTargetTypeConverter(inputType, target); + if (outputConverter != null) + { + return Attempt.Succeed(outputConverter.ConvertFrom(input!)); } - catch (Exception e) + + if (target.IsGenericType && GetCachedGenericNullableType(target) != null) { - return Attempt.Fail(e); + // 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.Fail(); + // 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); } - /// - /// 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) + 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) { - // Easy - if (target == typeof(string)) + 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)) { - return Attempt.Succeed(input); + // null/empty = bool false + return Attempt.Succeed(false); } - // Null, empty, whitespaces - if (string.IsNullOrWhiteSpace(input)) + if (target == typeof(DateTime)) { - if (target == typeof(bool)) - { - // null/empty = bool false - return Attempt.Succeed(false); - } + // null/empty = min DateTime value + return Attempt.Succeed(DateTime.MinValue); + } - if (target == typeof(DateTime)) + // 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)) { - // null/empty = min DateTime value - return Attempt.Succeed(DateTime.MinValue); + return Attempt.Succeed(value); } - // 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. + // 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)); } - // 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(long)) { - if (target == typeof(int)) + if (long.TryParse(input, out var value)) { - 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)); + return Attempt.Succeed(value); } - 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? + // Same as int + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt64(value2)); + } - if (target == typeof(bool)) + // TODO: Should we do the decimal trick for short, byte, unsigned? + if (target == typeof(bool)) + { + if (bool.TryParse(input, out var value)) { - if (bool.TryParse(input, out var value)) - { - return Attempt.Succeed(value); - } - - // Don't declare failure so the CustomBooleanTypeConverter can try - return null; + return Attempt.Succeed(value); } - // 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); + // Don't declare failure so the CustomBooleanTypeConverter can try + return null; + } - case TypeCode.Double: - var input2 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(double.TryParse(input2, out var valueD), valueD); + // 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.Single: - var input3 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(float.TryParse(input3, out var valueF), valueF); + case TypeCode.Double: + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(double.TryParse(input2, out var valueD), valueD); - case TypeCode.Char: - return Attempt.If(char.TryParse(input, out var valueC), valueC); + case TypeCode.Single: + var input3 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(float.TryParse(input3, out var valueF), valueF); - case TypeCode.Byte: - return Attempt.If(byte.TryParse(input, out var valueB), valueB); + case TypeCode.Char: + return Attempt.If(char.TryParse(input, out var valueC), valueC); - case TypeCode.SByte: - return Attempt.If(sbyte.TryParse(input, out var valueSb), valueSb); + case TypeCode.Byte: + return Attempt.If(byte.TryParse(input, out var valueB), valueB); - case TypeCode.UInt32: - return Attempt.If(uint.TryParse(input, out var valueU), valueU); + case TypeCode.SByte: + return Attempt.If(sbyte.TryParse(input, out var valueSb), valueSb); - case TypeCode.UInt16: - return Attempt.If(ushort.TryParse(input, out var valueUs), valueUs); + case TypeCode.UInt32: + return Attempt.If(uint.TryParse(input, out var valueU), valueU); - 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 var value), value); + 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(DateTime)) + } + 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)) { - if (DateTime.TryParse(input, out var value)) + switch (value.Kind) { - switch (value.Kind) - { - case DateTimeKind.Unspecified: - case DateTimeKind.Utc: - return Attempt.Succeed(value); + case DateTimeKind.Unspecified: + case DateTimeKind.Utc: + return Attempt.Succeed(value); - case DateTimeKind.Local: - return Attempt.Succeed(value.ToUniversalTime()); + case DateTimeKind.Local: + return Attempt.Succeed(value.ToUniversalTime()); - default: - throw new ArgumentOutOfRangeException(); - } + 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); - } - - //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) - { - return o?.ToDictionary(ignoreProperties.Select(e => o.GetPropertyInfo(e)).Select(propInfo => propInfo.Name).ToArray()); - } - - /// - /// Turns object into dictionary - /// - /// - /// Properties to ignore - /// - public static IDictionary ToDictionary(this object o, params string[] ignoreProperties) - { - if (o != null) + return Attempt.Fail(); + } + else if (target == typeof(DateTimeOffset)) + { + 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)) { - 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) { - var val = prop.GetValue(o); - if (val != null) - { - d.Add(prop.Name, (TVal)val); - } + d.Add(prop.Name, (TVal)val); } - return d; } - return new Dictionary(); + + return d; } + return new Dictionary(); + } + /// + /// 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 string.Empty; + } - internal static string? ToDebugString(this object? obj, int levels = 0) + if (type == typeof(string)) { - if (obj == null) return "{null}"; - try - { - 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 value.ToString().IsNullOrWhiteSpace() ? string.Empty : value.ToString()!; + } - return items.Any() - ? "{{ {0} }}".InvariantFormat(string.Join(", ", items)) - : null; - } + if (type == typeof(bool)) + { + return XmlConvert.ToString((bool)value); + } - 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(); - - return items.Any() - ? "[{0}]:{{ {1} }}".InvariantFormat(obj.GetType().Name, String.Join(", ", items)) - : null; - } - } - catch (Exception ex) - { - return "[Exception:{0}]".InvariantFormat(ex.Message); - } - return null; + 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); } - /// - /// Attempts to serialize the value to an XmlString using ToXmlString - /// - /// - /// - /// - internal static Attempt TryConvertToXmlString(this object value, Type type) + if (type == typeof(ushort)) { - try + 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 output = value.ToXmlString(type); - return Attempt.Succeed(output); + return "\"{0}\"".InvariantFormat(obj); } - catch (NotSupportedException ex) + + 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 Attempt.Fail(ex); + return "{0}".InvariantFormat(obj); } - } - /// - /// 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 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"); - } - - /// - /// Returns an XmlSerialized safe string representation for the value and type - /// - /// - /// - /// - public static string ToXmlString(this object value) - { - return value.ToXmlString(typeof (T)); - } - - private static string? GetEnumPropertyDebugString(object enumItem, int levels) - { - try + if (obj is Enum) { - return enumItem.ToDebugString(levels - 1); + return "[{0}]".InvariantFormat(obj); } - catch (Exception) + + if (obj is IEnumerable enumerable) { - return "[GetEnumPartException]"; + var items = (from object enumItem in enumerable + let value = GetEnumPropertyDebugString(enumItem, levels) + where value != null + select value).Take(10).ToList(); + + return items.Any() + ? "{{ {0} }}".InvariantFormat(string.Join(", ", items)) + : null; } - } - private static string? GetPropertyDebugString(PropertyInfo propertyInfo, object obj, int levels) - { - try + PropertyInfo[] props = obj.GetType().GetProperties(); + if (props.Length == 2 && props[0].Name == "Key" && props[1].Name == "Value" && levels > -2) { - return propertyInfo.GetValue(obj, null).ToDebugString(levels - 1); + 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) + + if (levels > -1) { - return "[GetPropertyValueException]"; + 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) + { + return "[Exception:{0}]".InvariantFormat(ex.Message); + } + + return null; + } - public static Guid AsGuid(this object value) + /// + /// Attempts to serialize the value to an XmlString using ToXmlString + /// + /// + /// + /// + internal static Attempt TryConvertToXmlString(this object value, Type type) + { + try + { + var output = value.ToXmlString(type); + return Attempt.Succeed(output); + } + catch (NotSupportedException ex) { - return value is Guid guid ? guid : Guid.Empty; + return Attempt.Fail(ex); } + } + + /// + /// 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; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static string NormalizeNumberDecimalSeparator(string s) + private static string? GetEnumPropertyDebugString(object enumItem, int levels) + { + try + { + return enumItem.ToDebugString(levels - 1); + } + catch (Exception) { - var normalized = System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0]; - return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized); + return "[GetEnumPartException]"; } + } - // 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) + private static string? GetPropertyDebugString(PropertyInfo propertyInfo, object obj, int levels) + { + try { - var key = new CompositeTypeTypeKey(source, target); + return propertyInfo.GetValue(obj, null).ToDebugString(levels - 1); + } + catch (Exception) + { + return "[GetPropertyValueException]"; + } + } - if (InputTypeConverterCache.TryGetValue(key, out var typeConverter)) - { - return typeConverter; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string NormalizeNumberDecimalSeparator(string s) + { + var normalized = Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0]; + return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized); + } - var converter = TypeDescriptor.GetConverter(source); - if (converter.CanConvertTo(target)) - { - return InputTypeConverterCache[key] = converter; - } + // 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); - InputTypeConverterCache[key] = null; - return null; + if (InputTypeConverterCache.TryGetValue(key, out TypeConverter? typeConverter)) + { + 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); + return InputTypeConverterCache[key] = converter; + } - if (DestinationTypeConverterCache.TryGetValue(key, out var typeConverter)) - { - return typeConverter; - } + InputTypeConverterCache[key] = null; + return null; + } - var converter = TypeDescriptor.GetConverter(target); - if (converter.CanConvertFrom(source)) - { - return DestinationTypeConverterCache[key] = converter; - } + // 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); - DestinationTypeConverterCache[key] = null; - return null; + if (DestinationTypeConverterCache.TryGetValue(key, out TypeConverter? typeConverter)) + { + return typeConverter; } - // 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) + TypeConverter converter = TypeDescriptor.GetConverter(target); + if (converter.CanConvertFrom(source)) { - if (NullableGenericCache.TryGetValue(type, out var underlyingType)) - { - return underlyingType; - } + return DestinationTypeConverterCache[key] = converter; + } - if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - Type? underlying = Nullable.GetUnderlyingType(type); - return NullableGenericCache[type] = underlying; - } + DestinationTypeConverterCache[key] = null; + return null; + } - NullableGenericCache[type] = 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)) + { + return underlyingType; } - // 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) + if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) { - var key = new CompositeTypeTypeKey(source, target); - if (AssignableTypeCache.TryGetValue(key, out var canConvert)) - { - return canConvert; - } + Type? underlying = Nullable.GetUnderlyingType(type); + return NullableGenericCache[type] = underlying; + } - // "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; - } + NullableGenericCache[type] = null; + return null; + } - return AssignableTypeCache[key] = false; + // 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; } - // determines whether a type can be converted to boolean - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool GetCachedCanConvertToBoolean(Type type) + // "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)) { - if (BoolConvertCache.TryGetValue(type, out var result)) - { - return result; - } + return AssignableTypeCache[key] = true; + } - if (CustomBooleanTypeConverter.CanConvertFrom(type)) - { - return BoolConvertCache[type] = true; - } + return AssignableTypeCache[key] = false; + } - return BoolConvertCache[type] = 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 0c15da6bdb9f..61b284383ab9 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}, - - // 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()}, + { "minPasswordLength", passwordConfiguration.RequiredLength }, - // 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}, - }; - } + // 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() }, - public static int GetMinNonAlphaNumericChars(this IPasswordConfiguration passwordConfiguration) - { - return passwordConfiguration.RequireNonLetterOrDigit ? 2 : 0; - } + // 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) => + passwordConfiguration.RequireNonLetterOrDigit ? 2 : 0; } diff --git a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs index 48769bda2c67..f7ad53d7d646 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) - { - 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; - } - - #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) + 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("", 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; + throw new ArgumentNullException(nameof(content)); } - #endregion - - #region Culture - - /// - /// Determines whether the content has a culture. - /// - /// Culture is case-insensitive. - public static bool HasCulture(this IPublishedContent content, string? culture) + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture()) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - return content.Cultures.ContainsKey(culture ?? string.Empty); + return content.Cultures.TryGetValue(string.Empty, out PublishedCultureInfo? invariantInfos) + ? invariantInfos.Name + : null; } - /// - /// 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 + // handle context culture for variant + if (culture == null) { - if (contents == null) throw new ArgumentNullException(nameof(contents)); - - culture = culture ?? variationContextAccessor.VariationContext?.Culture ?? ""; - - // either does not vary by culture, or has the specified culture - return contents.Where(x => !x.ContentType.VariesByCulture() || HasCulture(x, culture)); + culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } - /// - /// 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.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; - } + // get + return culture != string.Empty && content.Cultures.TryGetValue(culture, out PublishedCultureInfo? infos) + ? infos.Name + : null; + } - #endregion + #endregion - #region IsComposedOf + #region Url segment - /// - /// 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) + /// + /// 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) { - return content.ContentType.CompositionAliases.InvariantContains(alias); + throw new ArgumentNullException(nameof(content)); } - #endregion - - #region Template - - /// - /// Returns the current template Alias - /// - /// Empty string if none is set. - public static string GetTemplateAlias(this IPublishedContent content, IFileService fileService) + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture()) { - if (content.TemplateId.HasValue == false) - { - return string.Empty; - } - - var template = fileService.GetTemplate(content.TemplateId.Value); - return template?.Alias ?? string.Empty; + return content.Cultures.TryGetValue(string.Empty, out PublishedCultureInfo? invariantInfos) + ? invariantInfos.UrlSegment + : null; } - public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, - WebRoutingSettings webRoutingSettings, int templateId) + // handle context culture for variant + if (culture == null) { - return content.IsAllowedTemplate(contentTypeService, - webRoutingSettings.DisableAlternativeTemplates, - webRoutingSettings.ValidateAlternativeTemplates, templateId); + culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } - public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, int templateId) - { - if (disableAlternativeTemplates) - return content.TemplateId == templateId; - - if (content.TemplateId == templateId || !validateAlternativeTemplates) - return true; + // get + return culture != string.Empty && content.Cultures.TryGetValue(culture, out PublishedCultureInfo? infos) + ? infos.UrlSegment + : null; + } - var publishedContentContentType = contentTypeService.Get(content.ContentType.Id); - if (publishedContentContentType == null) - throw new NullReferenceException("No content type returned for published content (contentType='" + content.ContentType.Id + "')"); + #endregion - return publishedContentContentType.IsAllowedTemplate(templateId); + #region IsComposedOf - } - 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); + throw new ArgumentNullException(nameof(content)); + } - // 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) - return property?.GetValue(culture, segment); + #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."); } - /// - /// 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) + switch (content.ContentType.ItemType) { - var property = content.GetProperty(alias); + case PublishedItemType.Content: + return publishedUrlProvider.GetUrl(content, mode, culture); - // 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); + case PublishedItemType.Media: + return publishedUrlProvider.GetMediaUrl(content, mode, culture); - // 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) - otherwise, default - return property == null ? default : property.Value(publishedValueFallback, culture, segment); + default: + throw new NotSupportedException(); } + } - #endregion + #endregion - #region IsSomething: misc. + #region Culture - /// - /// 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) + /// + /// 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.ContentType.Alias.InvariantEquals(docTypeAlias); + throw new ArgumentNullException(nameof(content)); } - /// - /// 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 content.Cultures.ContainsKey(culture ?? string.Empty); + } - return recursive && content.IsComposedOf(docTypeAlias); + /// + /// 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); + + /// + /// 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)); } - #endregion + culture = culture ?? variationContextAccessor.VariationContext?.Culture ?? string.Empty; - #region IsSomething: equality + // either does not vary by culture, or has the specified culture + return contents.Where(x => !x.ContentType.VariesByCulture() || HasCulture(x, culture)); + } - public static bool IsEqual(this IPublishedContent content, IPublishedContent other) + /// + /// 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.Id == other.Id; + return content.UpdateDate; } - public static bool IsNotEqual(this IPublishedContent content, IPublishedContent other) + // handle context culture for variant + if (culture == null) { - return content.IsEqual(other) == false; + culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } - #endregion + // get + return culture != string.Empty && content.Cultures.TryGetValue(culture, out PublishedCultureInfo? infos) + ? infos.Date + : DateTime.MinValue; + } - #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(',')); - } - - public static bool IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other) - { - return content.Path.InvariantEquals(other.Path) || content.IsDescendant(other); - } + #region Template - public static bool IsAncestor(this IPublishedContent content, IPublishedContent other) - { - return content.Level < other.Level && other.Path.InvariantStartsWith(content.Path.EnsureEndsWith(',')); - } - - public static bool IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other) + /// + /// 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 other.Path.InvariantEquals(content.Path) || content.IsAncestor(other); + return 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) - { - return content.AncestorsOrSelf(false, null); - } + ITemplate? template = fileService.GetTemplate(content.TemplateId.Value); + return template?.Alias ?? string.Empty; + } - /// - /// 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); - } + public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, WebRoutingSettings webRoutingSettings, int templateId) => + content.IsAllowedTemplate(contentTypeService, webRoutingSettings.DisableAlternativeTemplates, webRoutingSettings.ValidateAlternativeTemplates, templateId); - /// - /// 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) + public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, int templateId) + { + if (disableAlternativeTemplates) { - return content.AncestorsOrSelf(false, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); + return content.TemplateId == templateId; } - /// - /// 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 + if (content.TemplateId == templateId || !validateAlternativeTemplates) { - return content.Ancestors().OfType(); + return true; } - /// - /// 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 + IContentType? publishedContentContentType = contentTypeService.Get(content.ContentType.Id); + if (publishedContentContentType == null) { - return content.Ancestors(maxLevel).OfType(); + throw new NullReferenceException("No content type returned for published content (contentType='" + + content.ContentType.Id + "')"); } - /// - /// 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); - } + return publishedContentContentType.IsAllowedTemplate(templateId); + } - /// - /// 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); - } + 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 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)); - } + #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) + { + IPublishedProperty? property = content.GetProperty(alias); - /// - /// 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 + // if we have a property, and it has a value, return that value + if (property != null && property.HasValue(culture, segment)) { - return content.AncestorsOrSelf().OfType(); + return true; } - /// - /// 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(); - } + // else let fallback try to get a value + return publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, null, out _, out _); + } - /// - /// 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; - } + /// + /// 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); - /// - /// 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) + // if we have a property, and it has a value, return that value + if (property != null && property.HasValue(culture, segment)) { - return content.EnumerateAncestors(false).FirstOrDefault(x => x.Level <= maxLevel); + return property.GetValue(culture, segment); } - /// - /// 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) + // else let fallback try to get a value + if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value, out property)) { - return content.EnumerateAncestors(false).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + return value; } - /// - /// 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(); - } + // 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); + } - /// - /// 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(); - } + /// + /// 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); - /// - /// 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) + // if we have a property, and it has a value, return that value + if (property != null && property.HasValue(culture, segment)) { - return content; + return property.Value(publishedValueFallback, culture, segment); } - /// - /// 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) + // else let fallback try to get a value + if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out T? value, out property)) { - return content.EnumerateAncestors(true).FirstOrDefault(x => x.Level <= maxLevel); + return value; } - /// - /// 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) - { - return content.EnumerateAncestors(true).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); - } + // 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); + } - /// - /// 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 + #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 content.AncestorsOrSelf().FirstOrDefault(); + return true; } - /// - /// 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(); - } + return recursive && content.IsComposedOf(docTypeAlias); + } - public static IEnumerable AncestorsOrSelf(this IPublishedContent content, bool orSelf, Func? func) - { - var ancestorsOrSelf = content.EnumerateAncestors(orSelf); - return func == null ? ancestorsOrSelf : ancestorsOrSelf.Where(func); - } + #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) + /// + /// 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) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (orSelf) yield return content; - while ((content = content.Parent) != null) - yield return content; + throw new ArgumentNullException(nameof(content)); } - #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) + if (orSelf) { - return content.AncestorsOrSelf(andSelf, null).Reverse(); + yield return content; } - /// - /// 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) + while ((content = content.Parent) != null) { - return content.AncestorsOrSelf(andSelf, n => n.Level >= minLevel).Reverse(); + yield return content; } + } - /// - /// 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 + #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) { - static IEnumerable TakeUntil(IEnumerable source, Func predicate) + foreach (IPublishedContent item in source) { - foreach (var item in source) + yield return item; + if (predicate(item)) { - yield return item; - if (predicate(item)) - { - yield break; - } + 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; - } + return TakeUntil(content.AncestorsOrSelf(andSelf, null), n => n is T).Reverse(); + } - public static T? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - where T : class, IPublishedContent + #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) { - return content.DescendantOrSelf(variationContextAccessor, level, culture) as T; + throw new ArgumentNullException(nameof(content)); } - internal static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, bool orSelf, Func? func, string? culture = null) + if (orSelf) { - return content.EnumerateDescendants(variationContextAccessor, orSelf, culture).Where(x => func == null || func(x)); + yield return content; } - internal static IEnumerable EnumerateDescendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, bool orSelf, string? culture = null) + IEnumerable? children = content.Children(variationContextAccessor, culture); + if (children is not 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 (IPublishedContent desc in children.SelectMany(x => + x.EnumerateDescendants(variationContextAccessor, culture))) { - foreach (var desc in children.SelectMany(x => x.EnumerateDescendants(variationContextAccessor, culture))) - yield return desc; + yield return desc; } } + } - internal static IEnumerable EnumerateDescendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + 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) { - yield return content; - var children = content.Children(variationContextAccessor, culture); - if (children is not null) + foreach (IPublishedContent desc in children.SelectMany(x => + x.EnumerateDescendants(variationContextAccessor, culture))) { - foreach (var desc in children.SelectMany(x => x.EnumerateDescendants(variationContextAccessor, culture))) - yield return desc; + 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 ?? ""; - - var children = content.ChildrenForAllCultures; - return culture == "*" - ? children - : children?.Where(x => x.IsInvariantOrHasCulture(culture)); - } - - /// - /// 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) + #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) { - return content.AncestorOrSelf(1); + culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } - /// - /// 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); - } + IEnumerable? children = content.ChildrenForAllCultures; + return culture == "*" + ? children + : children?.Where(x => x.IsInvariantOrHasCulture(culture)); + } - #endregion + /// + /// 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; + } - #region Writer and creator + public static string? GetWriterName(this IPublishedContent content, IUserService userService) + { + IProfile? user = userService.GetProfileById(content.WriterId); + return user?.Name; + } - public static string? GetCreatorName(this IPublishedContent content, IUserService userService) + #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) { - var user = userService.GetProfileById(content.CreatorId); - return user?.Name; + // No children found + return new DataTable(); } - public static string? GetWriterName(this IPublishedContent content, IUserService userService) - { - var user = userService.GetProfileById(content.WriterId); - return user?.Name; - } + // 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( - #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."); + // pass in the alias of the first child node since this is the node type we're rendering headers for + firstNode.ContentType.Alias, - switch (content.ContentType.ItemType) + // pass in the callback to extract the Dictionary of all defined aliases to their names + alias => GetPropertyAliasesAndNames(contentTypeService, mediaTypeService, memberTypeService, alias), + () => { - 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. - () => + // 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) { - //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 (IPublishedContent n in children) { - //loop through each child and create row data for it - foreach (var n in children) + if (contentTypeAliasFilter.IsNullOrWhiteSpace() == false) { - if (contentTypeAliasFilter.IsNullOrWhiteSpace() == false) + if (n.ContentType.Alias.InvariantEquals(contentTypeAliasFilter) == false) { - if (n.ContentType.Alias.InvariantEquals(contentTypeAliasFilter) == false) - continue; //skip this one, it doesn't match the filter + 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); + 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; - } + return tableData; + }); + return dt; + } - #endregion + #endregion - #region PropertyAliasesAndNames + #region PropertyAliasesAndNames - private static Func>? _getPropertyAliasesAndNames; + 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; - } + /// + /// 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); + 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 (var field in stdFields.Where(x => fields.ContainsKey(x.Key) == false)) - { - fields[field.Key] = field.Value; - } + // 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" }, + }; - return fields; + foreach (KeyValuePair field in stdFields.Where(x => fields.ContainsKey(x.Key) == false)) + { + fields[field.Key] = field.Value; } - private static Dictionary GetAliasesAndNames(IContentTypeBase? contentType) => contentType?.PropertyTypes.ToDictionary(x => x.Alias, x => x.Name) ?? new Dictionary(); - - #endregion + 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 17133cddaac3..c85178c85c49 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 + #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 + // 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 + return contents.Where(x => types.InvariantContains(x.ContentType.Alias)); + } - #region IsComposedOf + #endregion - /// - /// 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); - } + #region IsComposedOf - #endregion + /// + /// 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); - #region HasProperty + #endregion - /// - /// 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; - } + #region HasProperty - #endregion + /// + /// 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; - #region HasValue + #endregion - /// - /// 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); - } + #region HasValue - #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); + /// + /// 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 - // 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); + #region Value - // else let fallback try to get a value - if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value)) - return 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); - // 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); + // 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); } - #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) + // else let fallback try to get a value + if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value)) { - var property = content.GetProperty(alias); + return value; + } - // 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... 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); + } - // else let fallback try to get a value - if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value)) - return value; + #endregion - // 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); - } + #region Value - #endregion + /// + /// 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); - #region ToIndexedArray + // 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); + } - public static IndexedArrayItem[] ToIndexedArray(this IEnumerable source) - where TContent : class, IPublishedElement + // else let fallback try to get a value + if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out T? value)) { - var set = source.Select((content, index) => new IndexedArrayItem(content, index)).ToArray(); - foreach (var setItem in set) setItem.TotalCount = set.Length; - return set; + return value; } - #endregion + // 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 + #region ToIndexedArray - /// - /// 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) + 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) { - // 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; + setItem.TotalCount = set.Length; } - #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 set; + } - return publishedUrlProvider.GetMediaUrl(content, mode, culture, propertyAlias); + #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)); } - #endregion + 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 b4ffc401305d..0d84e3268ea6 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 it's not ILivePublishedModelFactory we know we're not using a live factory + return false; + } + + /// + /// 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) { - if (factory is IAutoPublishedModelFactory liveFactory) + lock (liveFactory.SyncRoot) { - lock (liveFactory.SyncRoot) - { - liveFactory.Reset(); + liveFactory.Reset(); - action(); - } - } - else - { action(); } } - + else + { + action(); + } } } diff --git a/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs b/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs index 3ff5c7771954..267157cf7acf 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 + #region Value - public static object? Value(this IPublishedProperty property, IPublishedValueFallback publishedValueFallback, string? culture = null, string? segment = null, Fallback fallback = default, object? defaultValue = default) + 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" + } + + #endregion - #region Value + #region Value - public static T? Value(this IPublishedProperty property, IPublishedValueFallback publishedValueFallback, string? culture = null, string? segment = null, Fallback fallback = default, T? defaultValue = default) + 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; - } + // 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; + } - var noValueConverted = noValue.TryConvertTo(); - if (noValueConverted.Success) - { - return noValueConverted.Result; - } + // we don't have a value, try fallback + if (publishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out T? fallbackValue)) + { + return fallbackValue; + } - // cannot cast noValue nor convert it, nothing we can return but 'default' - return default; + // 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; } - #endregion + // 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 9fd7da4640d6..5e6d356674a6 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 +namespace Umbraco.Extensions; + +public static class PublishedSnapshotAccessorExtensions { - public static class PublishedSnapshotAccessorExtensions + public static IPublishedSnapshot GetRequiredPublishedSnapshot( + this IPublishedSnapshotAccessor publishedSnapshotAccessor) { - public static IPublishedSnapshot GetRequiredPublishedSnapshot(this IPublishedSnapshotAccessor publishedSnapshotAccessor) + if (publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot)) { - if (publishedSnapshotAccessor.TryGetPublishedSnapshot(out var publishedSnapshot)) - { - return publishedSnapshot!; - } - - throw new InvalidOperationException("Wasn't possible to a get a valid Snapshot"); + 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 e9e6618f8cd0..475f093785b2 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}:"; + return RequestHandlerSettings.DefaultCharCollection; + } - IEnumerable charCollection = GetReplacements( - configuration, - $"{sectionKey}{nameof(RequestHandlerSettings.CharCollection)}"); + return MergeUnique( + requestHandlerSettings.UserDefinedCharCollection, + RequestHandlerSettings.DefaultCharCollection); + } - IEnumerable userDefinedCharCollection = GetReplacements( - configuration, - $"{sectionKey}{nameof(requestHandlerSettings.UserDefinedCharCollection)}"); + /// + /// Merges CharCollection and UserDefinedCharCollection, prioritizing UserDefinedCharCollection + /// + internal static void MergeReplacements( + this RequestHandlerSettings requestHandlerSettings, + IConfiguration configuration) + { + var sectionKey = $"{Constants.Configuration.ConfigRequestHandler}:"; - IEnumerable mergedCollection = MergeUnique(userDefinedCharCollection, charCollection); + IEnumerable charCollection = GetReplacements( + configuration, + $"{sectionKey}{nameof(RequestHandlerSettings.CharCollection)}"); - requestHandlerSettings.UserDefinedCharCollection = mergedCollection; - } + IEnumerable userDefinedCharCollection = GetReplacements( + configuration, + $"{sectionKey}{nameof(requestHandlerSettings.UserDefinedCharCollection)}"); - private static IEnumerable GetReplacements(IConfiguration configuration, string key) - { - var replacements = new List(); - IEnumerable config = configuration.GetSection(key).GetChildren(); + IEnumerable mergedCollection = MergeUnique(userDefinedCharCollection, charCollection); - 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 }); - } + requestHandlerSettings.UserDefinedCharCollection = mergedCollection; + } - return replacements; - } + private static IEnumerable GetReplacements(IConfiguration configuration, string key) + { + var replacements = new List(); + IEnumerable config = configuration.GetSection(key).GetChildren(); - /// - /// Merges two IEnumerable of CharItem without any duplicates, items in priorityReplacements will override those in alternativeReplacements - /// - private static IEnumerable MergeUnique( - IEnumerable priorityReplacements, - IEnumerable alternativeReplacements) + foreach (IConfigurationSection section in config) { - var priorityReplacementsList = priorityReplacements.ToList(); - var alternativeReplacementsList = alternativeReplacements.ToList(); + var @char = section.GetValue(nameof(CharItem.Char)); + var replacement = section.GetValue(nameof(CharItem.Replacement)); + replacements.Add(new CharItem { Char = @char, Replacement = replacement }); + } + + 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 72930b89f835..219b73c39f25 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; + + // 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 Umbraco is greater than - /// - public static bool UmbracoCanBoot(this IRuntimeState state) => state.Level > RuntimeLevel.BootFailed; + /// + /// 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; - } + /// + /// 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 e8b2a2534bce..afdd49612ee0 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 +namespace Umbraco.Extensions; + +public static class SemVersionExtensions { - public static class SemVersionExtensions - { - public static string ToSemanticString(this SemVersion semVersion) - { - return semVersion.ToString().Replace("--", "-").Replace("-+", "+"); - } + 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.Substring(0, indexOfBuild) : version; - } + 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 c41bc290ff16..694b4d05e669 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 - { - private const char DefaultEscapedStringEscapeChar = '\\'; - private static readonly char[] ToCSharpHexDigitLower = "0123456789abcdef".ToCharArray(); - private static readonly char[] ToCSharpEscapeChars; + internal static readonly Lazy Whitespace = new(() => new Regex(@"\s+", RegexOptions.Compiled)); - static StringExtensions() - { - 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]; - } + 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"); - /// - /// Convert a path to node ids in the order from right to left (deepest to shallowest) - /// - /// - /// - public static int[] GetIdsFromPathReversed(this string path) + 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 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; + ToCSharpEscapeChars[escape[0]] = escape[1]; } + } - /// - /// Removes new lines and tabs - /// - /// - /// - public static string StripWhitespace(this string txt) + /// + /// 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")) { - return Regex.Replace(txt, @"\s", string.Empty); + return fileName; } - public static string StripFileExtension(this string fileName) + var lastIndex = fileName.LastIndexOf('.'); + if (lastIndex > 0) { - //filenames cannot contain line breaks - if (fileName.Contains(Environment.NewLine) || fileName.Contains("\r") || fileName.Contains("\n")) return fileName; + var ext = fileName.Substring(lastIndex); - var lastIndex = fileName.LastIndexOf('.'); - if (lastIndex > 0) + // file extensions cannot contain whitespace + if (ext.Contains(" ")) { - 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; } - 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) + { + // 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; + } - /// - /// Determines the extension of the path or URL - /// - /// - /// Extension of the file - public static string GetFileExtension(this string file) + /// + /// 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()) { - //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; + return false; } - /// - /// 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("]")); - } + 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 bool DetectIsEmptyJson(this string input) => + JsonEmpties.Contains(Whitespace.Value.Replace(input, string.Empty)); - public static string ReplaceNonAlphanumericChars(this string input, string replacement) + 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))) { - //any character that is not alphanumeric, convert to a hyphen - var mName = input; - foreach (var c in mName.ToCharArray().Where(c => !char.IsLetterOrDigit(c))) - { - mName = mName.Replace(c.ToString(CultureInfo.InvariantCulture), replacement); - } - return mName; + mName = mName.Replace(c.ToString(CultureInfo.InvariantCulture), replacement); } - public static string ReplaceNonAlphanumericChars(this string input, char 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++) { - 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); + outputArray[i] = char.IsLetterOrDigit(inputArray[i]) ? inputArray[i] : replacement; } - private static readonly char[] CleanForXssChars = "*?(){}[];:%<>/\\|&'\"".ToCharArray(); - /// - /// Cleans string to aid in preventing xss attacks. - /// - /// - /// - /// - public static string CleanForXss(this string input, params char[] ignoreFromClean) + return new string(outputArray); + } + + /// + /// 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)) { - //remove any HTML - input = input.StripHtml(); - //strip out any potential chars involved with XSS - return input.ExceptChars(new HashSet(CleanForXssChars.Except(ignoreFromClean))); + sb.Append(c); } - public static string ExceptChars(this string str, HashSet toExclude) + return sb.ToString(); + } + + /// + /// 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++) { - 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); - } + queryStrings[i] = queryStrings[i].TrimStart(Constants.CharArrays.QuestionMarkAmpersand) + .TrimEnd(Constants.CharArrays.Ampersand); + } - var nonEmpty = queryStrings.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); + var nonEmpty = queryStrings.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); - if (url.Contains("?")) - { - return url + string.Join("&", nonEmpty).EnsureStartsWith('&'); - } - return url + string.Join("&", nonEmpty).EnsureStartsWith('?'); + if (url.Contains("?")) + { + return url + string.Join("&", nonEmpty).EnsureStartsWith('&'); } + 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) + // 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)) { - 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"; - } + name += "es"; return name; } - public static bool IsVowel(this char c) + if (name.EndsWith("y", StringComparison.OrdinalIgnoreCase) && name.Length > 1 && + !IsVowel(name[^2])) { - 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; + name = name.Remove(name.Length - 1, 1); + name += "ies"; + return name; } - /// - /// 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 (!name.EndsWith("s", StringComparison.OrdinalIgnoreCase)) { - if (string.IsNullOrEmpty(value)) return value; - return value.TrimEnd(forRemoving).TrimStart(forRemoving); + name += "s"; } - public static string EncodeJsString(this string s) - { - 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(); + return name; + } + + public static bool IsVowel(this char c) + { + 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; } - public static string TrimEnd(this string value, string forRemoving) - { - if (string.IsNullOrEmpty(value)) return value; - if (string.IsNullOrEmpty(forRemoving)) return value; + return false; + } - while (value.EndsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase)) - { - value = value.Remove(value.LastIndexOf(forRemoving, StringComparison.InvariantCultureIgnoreCase)); - } + /// + /// 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; } - public static string TrimStart(this string value, string forRemoving) - { - if (string.IsNullOrEmpty(value)) return value; - if (string.IsNullOrEmpty(forRemoving)) return value; + return value.TrimEnd(forRemoving).TrimStart(forRemoving); + } - while (value.StartsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase)) + public static string EncodeJsString(this string s) + { + var sb = new StringBuilder(); + foreach (var c in s) + { + switch (c) { - value = value.Substring(forRemoving.Length); + 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; } - return value; } - public static string EnsureStartsWith(this string input, string toStartWith) + return sb.ToString(); + } + + public static string TrimEnd(this string value, string forRemoving) + { + if (string.IsNullOrEmpty(value)) { - if (input.StartsWith(toStartWith)) return input; - return toStartWith + input.TrimStart(toStartWith); + return value; } - public static string EnsureStartsWith(this string input, char value) + if (string.IsNullOrEmpty(forRemoving)) { - return input.StartsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : value + input; + return value; } - public static string EnsureEndsWith(this string input, char value) + while (value.EndsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase)) { - return input.EndsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : input + value; + value = value.Remove(value.LastIndexOf(forRemoving, StringComparison.InvariantCultureIgnoreCase)); } - public static string EnsureEndsWith(this string input, string toEndWith) + return value; + } + + public static string TrimStart(this string value, string forRemoving) + { + if (string.IsNullOrEmpty(value)) { - return input.EndsWith(toEndWith.ToString(CultureInfo.InvariantCulture)) ? input : input + toEndWith; + return value; } - public static bool IsLowerCase(this char ch) + if (string.IsNullOrEmpty(forRemoving)) { - return ch.ToString(CultureInfo.InvariantCulture) == ch.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(); + return value; } - public static bool IsUpperCase(this char ch) + while (value.StartsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase)) { - return ch.ToString(CultureInfo.InvariantCulture) == ch.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(); + value = value.Substring(forRemoving.Length); } - /// 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); + return value; + } - public static string? IfNullOrWhiteSpace(this string? str, string? defaultValue) + public static string EnsureStartsWith(this string input, string toStartWith) + { + if (input.StartsWith(toStartWith)) { - return str.IsNullOrWhiteSpace() ? defaultValue : str; + return input; } - /// 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(); - } + return toStartWith + input.TrimStart(toStartWith); + } - /// 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(T); - return false; - } - } + 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(); + } - /// - /// 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) + /// 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 { - return (T)Enum.Parse(typeof(T), strType, ignoreCase); + result = (T)Enum.Parse(typeof(T), strType, ignoreCase); + return true; } - - /// - /// Strips all HTML from a string. - /// - /// The text. - /// Returns the string without any HTML tags. - public static string StripHtml(this string text) + catch { - const string pattern = @"<(.|\n)*?>"; - return Regex.Replace(text, pattern, string.Empty, RegexOptions.Compiled); + result = default; + return false; } + } - /// - /// 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'); - var output = Guid.Empty; - return Guid.TryParse(hex, out output) ? output : Guid.Empty; - } + /// + /// 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); + } - /// - /// Converts to hex. - /// - /// The input. - /// - public static string ConvertToHex(this string input) + /// + /// Encodes as GUID. + /// + /// The input. + /// + public static Guid EncodeAsGuid(this string input) + { + if (string.IsNullOrWhiteSpace(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(); + throw new ArgumentNullException("input"); } - public static string DecodeFromHex(this string hexValue) - { - var strValue = ""; - while (hexValue.Length > 0) - { - strValue += Convert.ToChar(Convert.ToUInt32(hexValue.Substring(0, 2), 16)).ToString(); - hexValue = hexValue.Substring(2, hexValue.Length - 2); - } - return strValue; - } + 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; + } - /// - /// Encodes a string to a safe URL base64 string - /// - /// - /// - public static string ToUrlBase64(this string input) + /// + /// Converts to hex. + /// + /// The input. + /// + public static string ConvertToHex(this string input) + { + if (string.IsNullOrEmpty(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); + return string.Empty; } - /// - /// Decodes a URL safe base64 string back - /// - /// - /// - public static string? FromUrlBase64(this string input) + var sb = new StringBuilder(input.Length); + foreach (var c in 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; - } + sb.AppendFormat("{0:x2}", Convert.ToUInt32(c)); } - /// - /// formats the string with invariant culture - /// - /// The format. - /// The args. - /// - public static string InvariantFormat(this string? format, params object?[] args) - { - return string.Format(CultureInfo.InvariantCulture, format ?? string.Empty, args); - } + return sb.ToString(); + } - /// - /// Converts an integer to an invariant formatted string - /// - /// - /// - public static string ToInvariantString(this int str) + public static string DecodeFromHex(this string hexValue) + { + var strValue = string.Empty; + while (hexValue.Length > 0) { - return str.ToString(CultureInfo.InvariantCulture); + strValue += Convert.ToChar(Convert.ToUInt32(hexValue.Substring(0, 2), 16)).ToString(); + hexValue = hexValue.Substring(2, hexValue.Length - 2); } - public static string ToInvariantString(this long str) + return strValue; + } + + /// + /// Encodes a string to a safe URL base64 string + /// + /// + /// + public static string ToUrlBase64(this string input) + { + if (input == null) { - return str.ToString(CultureInfo.InvariantCulture); + throw new ArgumentNullException(nameof(input)); } - /// - /// Compares 2 strings with invariant culture and case ignored - /// - /// The compare. - /// The compare to. - /// - public static bool InvariantEquals(this string? compare, string? compareTo) + if (string.IsNullOrEmpty(input)) { - return String.Equals(compare, compareTo, StringComparison.InvariantCultureIgnoreCase); + return string.Empty; } - public static bool InvariantStartsWith(this string compare, string compareTo) + // 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) { - return compare.StartsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); + throw new ArgumentNullException(nameof(input)); } - public static bool InvariantEndsWith(this string compare, string compareTo) + // if (input.IsInvalidBase64()) return null; + try { - return compare.EndsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); + // var decodedBytes = Convert.FromBase64String(input.Replace("-", ".").Replace("_", "/").Replace(",", "=")); + var decodedBytes = UrlTokenDecode(input); + return decodedBytes != null ? Encoding.UTF8.GetString(decodedBytes) : null; } - - public static bool InvariantContains(this string compare, string compareTo) + catch (FormatException) { - return compare.IndexOf(compareTo, StringComparison.OrdinalIgnoreCase) >= 0; + return null; } + } - public static bool InvariantContains(this IEnumerable compare, string compareTo) + /// + /// 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) { - return compare.Contains(compareTo, StringComparer.InvariantCultureIgnoreCase); + TypeConverter tc = TypeDescriptor.GetConverter(type); + return tc.ConvertFrom(val); } - public static int InvariantIndexOf(this string s, string value) + 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) { - return s.IndexOf(value, StringComparison.OrdinalIgnoreCase); + throw new ArgumentNullException(nameof(input)); } - public static int InvariantLastIndexOf(this string s, string value) + if (input.Length == 0) { - return s.LastIndexOf(value, StringComparison.OrdinalIgnoreCase); + return Array.Empty(); } - - /// - /// 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) + // calc array size - must be groups of 4 + var arrayLength = input.Length; + var remain = arrayLength % 4; + if (remain != 0) { - return (T?)val.ParseInto(typeof(T)); + arrayLength += 4 - remain; } - /// - /// 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) + var inArray = new char[arrayLength]; + for (var i = 0; i < input.Length; i++) { - 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) + var ch = input[i]; + switch (ch) { - //convert our string into byte array - var byteArray = Encoding.UTF8.GetBytes(str); + case '-': // restore '-' as '+' + inArray[i] = '+'; + break; - //get the hashed values created by our selected provider - var hashedByteArray = hasher.ComputeHash(byteArray); + case '_': // restore '_' as '/' + inArray[i] = '/'; + break; - //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(); + default: // keep char unchanged + inArray[i] = ch; + break; } } - /// - /// Decodes a string that was encoded with UrlTokenEncode - /// - /// - /// - public static byte[] UrlTokenDecode(this string input) + // pad with '=' + for (var j = input.Length; j < inArray.Length; j++) { - 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; + inArray[j] = '='; + } - 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; - } - } + return Convert.FromBase64CharArray(inArray, 0, inArray.Length); + } - // pad with '=' - for (var j = input.Length; j < inArray.Length; j++) - inArray[j] = '='; + /// + /// 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; - return Convert.FromBase64CharArray(inArray, 0, inArray.Length); + // create an instance of the correct hashing provider based on the type passed in + if (hashType is not null) + { + hasher = HashAlgorithm.Create(hashType); } - /// - /// Encodes a string so that it is 'safe' for URLs, files, etc.. - /// - /// - /// - public static string UrlTokenEncode(this byte[] input) + if (hasher == null) { - if (input == null) - throw new ArgumentNullException(nameof(input)); - - if (input.Length == 0) - return string.Empty; + throw new InvalidOperationException("No hashing type found by name " + hashType); + } - // base-64 digits are A-Z, a-z, 0-9, + and / - // the = char is used for trailing padding + using (hasher) + { + // convert our string into byte array + var byteArray = Encoding.UTF8.GetBytes(str); - var str = Convert.ToBase64String(input); + // get the hashed values created by our selected provider + var hashedByteArray = hasher.ComputeHash(byteArray); - var pos = str.IndexOf('='); - if (pos < 0) pos = str.Length; + // create a StringBuilder object + var stringBuilder = new StringBuilder(); - // replace chars that would cause problems in URLs - var chArray = new char[pos]; - for (var i = 0; i < pos; i++) + // loop to each byte + foreach (var b in hashedByteArray) { - 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; - } + // append it to our StringBuilder + stringBuilder.Append(b.ToString("x2")); } - return new string(chArray); + // return the hashed value + return stringBuilder.ToString(); } + } - /// - /// Ensures that the folder path ends with a DirectorySeparatorChar - /// - /// - /// - public static string NormaliseDirectoryPath(this string currentFolder) + /// + /// Encodes a string so that it is 'safe' for URLs, files, etc.. + /// + /// + /// + public static string UrlTokenEncode(this byte[] input) + { + if (input == null) { - currentFolder = currentFolder - .IfNull(x => String.Empty) - .TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; - return currentFolder; + throw new ArgumentNullException(nameof(input)); } - /// - /// Truncates the specified text string. - /// - /// The text. - /// Length of the max. - /// The suffix. - /// - public static string Truncate(this string text, int maxLength, string suffix = "...") + if (input.Length == 0) { - // replaces the truncated string to a ... - var truncatedString = text; + return string.Empty; + } - if (maxLength <= 0) return truncatedString; - var strLength = maxLength - suffix.Length; + // base-64 digits are A-Z, a-z, 0-9, + and / + // the = char is used for trailing padding + var str = Convert.ToBase64String(input); - if (strLength <= 0) return truncatedString; + var pos = str.IndexOf('='); + if (pos < 0) + { + pos = str.Length; + } - if (text == null || text.Length <= maxLength) return truncatedString; + // 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; - truncatedString = text.Substring(0, strLength); - truncatedString = truncatedString.TrimEnd(); - truncatedString += suffix; + case '/': // replace '/' with '_' + chArray[i] = '_'; + break; - return truncatedString; + default: // keep char unchanged + chArray[i] = ch; + break; + } } - /// - /// Strips carrage returns and line feeds from the specified text. - /// - /// The input. - /// - public static string StripNewLines(this string input) + 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 input.Replace("\r", "").Replace("\n", ""); + return truncatedString; } - /// - /// Converts to single line by replacing line breaks with spaces. - /// - public static string ToSingleLine(this string text) + var strLength = maxLength - suffix.Length; + + if (strLength <= 0) { - 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; + return truncatedString; } - public static string OrIfNullOrWhiteSpace(this string input, string alternative) + if (text == null || text.Length <= maxLength) { - return !string.IsNullOrWhiteSpace(input) - ? input - : alternative; + return truncatedString; } - /// - /// 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) + 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)) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToUpper() + input.Substring(1); + return text; } - /// - /// 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) + 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 - : input.Substring(0, 1).ToLower() + input.Substring(1); + throw new ArgumentNullException(nameof(text)); } - /// - /// 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) + if (replacements == null) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToUpper(culture) + input.Substring(1); + throw new ArgumentNullException(nameof(replacements)); } - /// - /// 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) + foreach (KeyValuePair item in replacements) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToLower(culture) + 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 invariant culture. - /// - /// The string. - /// The converted string. - public static string ToFirstUpperInvariant(this string input) + 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).ToUpperInvariant() + 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 invariant culture. - /// - /// The string. - /// The converted string. - public static string ToFirstLowerInvariant(this string input) + if (chars == null) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToLowerInvariant() + input.Substring(1); + throw new ArgumentNullException(nameof(chars)); } - /// - /// 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) + for (var i = 0; i < chars.Length; i++) { - 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; + text = text.Replace(chars[i], replacement); } - /// - /// 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) throw new ArgumentNullException(nameof(text)); - if (chars == null) throw new ArgumentNullException(nameof(chars)); + 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) + { + throw new ArgumentNullException(nameof(text)); + } - for (int i = 0; i < chars.Length; i++) - text = text.Replace(chars[i], replacement); + var pos = text.IndexOf(search, StringComparison.InvariantCulture); + if (pos < 0) + { 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) throw new ArgumentNullException(nameof(text)); + return text.Substring(0, pos) + replace + text.Substring(pos + search.Length); + } - var pos = text.IndexOf(search, StringComparison.InvariantCulture); + /// + /// 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; - if (pos < 0) - return text; + // 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) + { + // Remove the old text. + source = source.Remove(index, oldString.Length); - return text.Substring(0, pos) + replace + text.Substring(pos + search.Length); + // Add the replacement text. + source = source.Insert(index, newString); } + return source; + } - - /// - /// 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) + /// + /// 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) { - // 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) - { - // Remove the old text. - source = source.Remove(index, oldString.Length); - - // Add the replacement text. - source = source.Insert(index, newString); - } - - return source; + return ""; } - /// - /// 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) + // 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++) { - 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++) + var c = s[rp]; + if (c < ToCSharpEscapeChars.Length && ToCSharpEscapeChars[c] != '\0') { - 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('\\').Append(ToCSharpEscapeChars[c]); } - - return sb.ToString(); - - // requires full trust - /* - using (var writer = new StringWriter()) - using (var provider = CodeDomProvider.CreateProvider("CSharp")) + else if (c <= '~' && c >= ' ') { - provider.GenerateCodeFromExpression(new CodePrimitiveExpression(s), writer, null); - return writer.ToString().Replace(string.Format("\" +{0}\t\"", Environment.NewLine), ""); + sb.Append(c); } - */ - } - - 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"); - - if (string.IsNullOrEmpty(haystack) || needles == null || !needles.Any()) + else { - return false; + sb.Append(@"\x") + .Append(ToCSharpHexDigitLower[(c >> 12) & 0x0F]) + .Append(ToCSharpHexDigitLower[(c >> 8) & 0x0F]) + .Append(ToCSharpHexDigitLower[(c >> 4) & 0x0F]) + .Append(ToCSharpHexDigitLower[c & 0x0F]); } - - return needles.Any(value => haystack.IndexOf(value, comparison) >= 0); } - public static bool CsvContains(this string csv, string value) + return sb.ToString(); + + // requires full trust + /* + using (var writer = new StringWriter()) + using (var provider = CodeDomProvider.CreateProvider("CSharp")) { - if (string.IsNullOrEmpty(csv)) - { - return false; - } - var idCheckList = csv.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - return idCheckList.Contains(value); + provider.GenerateCodeFromExpression(new CodePrimitiveExpression(s), writer, null); + return writer.ToString().Replace(string.Format("\" +{0}\t\"", Environment.NewLine), ""); } + */ + } - /// - /// Converts a file name to a friendly name for a content item - /// - /// - /// - public static string ToFriendlyName(this string fileName) + 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) { - // strip the file extension - fileName = fileName.StripFileExtension(); + throw new ArgumentNullException("haystack"); + } - // underscores and dashes to spaces - fileName = fileName.ReplaceMany(Constants.CharArrays.UnderscoreDash, ' '); + if (string.IsNullOrEmpty(haystack) || needles == null || !needles.Any()) + { + return false; + } - // any other conversions ? + return needles.Any(value => haystack.IndexOf(value, comparison) >= 0); + } - // Pascalcase (to be done last) - fileName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(fileName); + public static bool CsvContains(this string csv, string value) + { + if (string.IsNullOrEmpty(csv)) + { + return false; + } - // Replace multiple consecutive spaces with a single space - fileName = string.Join(" ", fileName.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries)); + var idCheckList = csv.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + return idCheckList.Contains(value); + } - return fileName; - } + /// + /// 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(); - // 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()) - { - algorithm.TransformBlock(namespaceBytes, 0, namespaceBytes.Length, null, 0); - algorithm.TransformFinalBlock(nameBytes, 0, nameBytes.Length); - hash = algorithm.Hash!; - } + // underscores and dashes to spaces + fileName = fileName.ReplaceMany(Constants.CharArrays.UnderscoreDash, ' '); - // 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); + // any other conversions ? - // 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)); + // Pascalcase (to be done last) + fileName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(fileName); - // 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); + // Replace multiple consecutive spaces with a single space + fileName = string.Join(" ", fileName.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries)); - // convert the resulting UUID to local byte order (step 13) - SwapByteOrder(newGuid); - return new Guid(newGuid); - } + return fileName; + } - // Converts a GUID (expressed as a byte array) to/from network order (MSB-first). - internal static void SwapByteOrder(byte[] guid) + /// + /// 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) { - SwapBytes(guid, 0, 3); - SwapBytes(guid, 1, 2); - SwapBytes(guid, 4, 5); - SwapBytes(guid, 6, 7); + throw new ArgumentNullException("name"); } - private static void SwapBytes(byte[] guid, int left, int right) + if (version != 3 && version != 5) { - byte temp = guid[left]; - guid[left] = guid[right]; - guid[right] = temp; + throw new ArgumentOutOfRangeException("version", "version must be either 3 or 5."); } - /// - /// Turns an null-or-whitespace string into a null string. - /// - public static string? NullOrWhiteSpaceAsNull(this string text) - => string.IsNullOrWhiteSpace(text) ? null : text; + // 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); - /// - /// 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) + // 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()) { - return string.IsNullOrWhiteSpace(path) == false - && path.IndexOfAny(Path.GetInvalidPathChars().ToArray()) == -1 - && Path.IsPathRooted(path) - && Path.GetPathRoot(path)?.Equals(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) == false; + algorithm.TransformBlock(namespaceBytes, 0, namespaceBytes.Length, null, 0); + algorithm.TransformFinalBlock(nameBytes, 0, nameBytes.Length); + hash = algorithm.Hash!; } - // FORMAT STRINGS + // 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); - /// - /// 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); - } + // 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); - // the new methods to get a url segment + // convert the resulting UUID to local byte order (step 13) + SwapByteOrder(newGuid); + return new Guid(newGuid); + } - /// - /// 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)); + // 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); + } - return shortStringHelper.CleanStringForUrlSegment(text); - } + private static void SwapBytes(byte[] guid, int left, int right) + { + var temp = guid[left]; + guid[left] = guid[right]; + guid[right] = temp; + } - /// - /// 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) + /// + /// 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) { - 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); + return a; } + return char.ToLowerInvariant(a[0]) + a.Substring(1); + } - /// - /// 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) + /// + /// 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) { - return shortStringHelper.CleanString(text, stringType); + throw new ArgumentNullException(nameof(text)); } - /// - /// 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) + if (string.IsNullOrWhiteSpace(text)) { - return shortStringHelper.CleanString(text, stringType, separator); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(text)); } - /// - /// 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); - } + return shortStringHelper.CleanStringForUrlSegment(text); + } - /// - /// 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) + /// + /// 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) { - return shortStringHelper.CleanString(text, stringType, separator, culture); + throw new ArgumentNullException(nameof(text)); } - // 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) + if (string.IsNullOrWhiteSpace(text)) { - 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(); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(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. - public static string ToSafeFileName(this string text, IShortStringHelper shortStringHelper) + 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) { - return shortStringHelper.CleanStringForSafeFileName(text); + yield break; } - /// - /// 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; + var sb = new StringBuilder(value.Length); + var escaped = false; - foreach (var chr in value.ToCharArray()) + foreach (var chr in value.ToCharArray()) + { + if (escaped) { - 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); - } + 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); } - - 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 1c585a2de892..b1e5515b8896 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 - 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; + // get the current culture + CultureInfo currentCulture = CultureInfo.CurrentCulture; - // 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 + // 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) + { + invariantCulture = invariantCulture.Parent; + } - thread.CurrentCulture = CultureInfo.GetCultureInfo(thread.CurrentCulture.Name); - thread.CurrentUICulture = CultureInfo.GetCultureInfo(thread.CurrentUICulture.Name); + // 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 bb43c2b5d94f..e3da8d9ee11c 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); - - foreach (var method in methods) - { - if (method.HasParameters(parameterTypes)) - return method; - } + 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; + } - return null; + public static IEnumerable GetBaseTypes(this Type? type, bool andSelf) + { + if (andSelf) + { + yield return type; } - /// - /// 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) + while ((type = type?.BaseType) != 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; + yield return type; } + } + internal static MethodInfo? GetGenericMethod(this Type type, string name, params Type[] parameterTypes) + { + IEnumerable methods = type.GetMethods().Where(method => method.Name == name); - - /// - /// Determines whether the specified type is enumerable. - /// - /// The type. - /// - internal static bool HasParameters(this MethodInfo method, params Type[] parameterTypes) + foreach (MethodInfo method in methods) { - var methodParameters = method.GetParameters().Select(parameter => parameter.ParameterType).ToArray(); + if (method.HasParameters(parameterTypes)) + { + return method; + } + } - if (methodParameters.Length != parameterTypes.Length) - return false; + return null; + } - for (int i = 0; i < methodParameters.Length; i++) - if (methodParameters[i].ToString() != parameterTypes[i].ToString()) - return false; + /// + /// 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(); - return true; + if (methodParameters.Length != parameterTypes.Length) + { + return false; } - public static IEnumerable GetBaseTypes(this Type? type, bool andSelf) + for (var i = 0; i < methodParameters.Length; i++) { - if (andSelf) - yield return type; - - while ((type = type?.BaseType) != null) - yield return type; + if (methodParameters[i].ToString() != parameterTypes[i].ToString()) + { + return false; + } } - 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 true; + } - return allTypes.SelectMany(t => t.GetMethods()); - } + 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) + /// + /// true if the specified type is enumerable; otherwise, false. + /// + public static bool IsEnumerable(this Type type) + { + if (type.IsGenericType) { - if (type.IsGenericType) + if (type.GetGenericTypeDefinition().GetInterfaces().Contains(typeof(IEnumerable))) { - if (type.GetGenericTypeDefinition().GetInterfaces().Contains(typeof(IEnumerable))) - return true; + return true; } - else + } + else + { + if (type.GetInterfaces().Contains(typeof(IEnumerable))) { - if (type.GetInterfaces().Contains(typeof(IEnumerable))) - return true; + 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 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) { - Type[]? args; - return type.TryGetGenericArguments(genericType, out args); + throw new ArgumentNullException("type"); } - /// - /// 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 (genericType == null) { - 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"); - } + throw new ArgumentNullException("genericType"); + } + + if (genericType.IsGenericType == false) + { + throw new ArgumentException("genericType must be a generic type"); + } - Func checkGenericType = (@int, t) => + Func checkGenericType = (@int, t) => + { + if (@int.IsGenericType) { - if (@int.IsGenericType) + Type def = @int.GetGenericTypeDefinition(); + if (def == t) { - var def = @int.GetGenericTypeDefinition(); - if (def == t) - { - return @int.GetGenericArguments(); - } + 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; + 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) + // if we're looking for interfaces, enumerate them: + if (genericType.IsInterface) + { + foreach (Type @interface in type.GetInterfaces()) { - foreach (Type @interface in type.GetInterfaces()) + genericArgType = checkGenericType(@interface, genericType); + if (genericArgType != null) { - genericArgType = checkGenericType(@interface, genericType); - if (genericArgType != null) - return true; + return true; } } - else + } + else + { + // loop back into the base types as long as they are generic + while (type.BaseType != null && type.BaseType != typeof(object)) { - //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) { - genericArgType = checkGenericType(type.BaseType, genericType); - if (genericArgType != null) - return true; - type = type.BaseType; + 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) + 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) { - if (type.IsInterface) - { - var propertyInfos = new List(); + var propertyInfos = new List(); - var considered = new List(); - var queue = new Queue(); - considered.Add(type); - queue.Enqueue(type); - while (queue.Count > 0) + 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.NonPublic - | BindingFlags.Instance); + considered.Add(subInterface); + queue.Enqueue(subInterface); + } - var newPropertyInfos = typeProperties - .Where(x => !propertyInfos.Contains(x)); + PropertyInfo[] typeProperties = subType.GetProperties( + BindingFlags.FlattenHierarchy + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.Instance); - propertyInfos.InsertRange(0, newPropertyInfos); - } + IEnumerable newPropertyInfos = typeProperties + .Where(x => !propertyInfos.Contains(x)); - return propertyInfos.ToArray(); + 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 propertyInfos = new List(); - var considered = new List(); - var queue = new Queue(); - considered.Add(type); - queue.Enqueue(type); - while (queue.Count > 0) + 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); + considered.Add(subInterface); + queue.Enqueue(subInterface); + } - var newPropertyInfos = typeProperties - .Where(x => !propertyInfos.Contains(x)); + PropertyInfo[] typeProperties = subType.GetProperties( + BindingFlags.FlattenHierarchy + | BindingFlags.Public + | BindingFlags.Instance); - propertyInfos.InsertRange(0, newPropertyInfos); - } + IEnumerable newPropertyInfos = typeProperties + .Where(x => !propertyInfos.Contains(x)); - return propertyInfos.ToArray(); + 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 TypeHelper.IsTypeAssignableFrom(actualType); - } + return type.GetProperties(BindingFlags.FlattenHierarchy + | BindingFlags.Public | BindingFlags.Instance); + } - public static bool Inherits(this Type type) - { - return typeof(TBase).IsAssignableFrom(type); - } + /// + /// 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, Type tbase) - { - return tbase.IsAssignableFrom(type); - } + public static bool Inherits(this Type type) => typeof(TBase).IsAssignableFrom(type); - public static bool Implements(this Type type) - { - return typeof(TInterface).IsAssignableFrom(type); - } + public static bool Inherits(this Type type, Type tbase) => tbase.IsAssignableFrom(type); - public static TAttribute? FirstAttribute(this Type type) - { - return type.FirstAttribute(true); - } + public static bool Implements(this Type type) => typeof(TInterface).IsAssignableFrom(type); - 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 Type type) => type.FirstAttribute(true); - public static TAttribute? FirstAttribute(this PropertyInfo propertyInfo) - { - return propertyInfo.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, bool inherit) - { - var attrs = propertyInfo.GetCustomAttributes(typeof(TAttribute), inherit); - return (TAttribute?)(attrs.Length > 0 ? attrs[0] : null); - } + public static TAttribute? FirstAttribute(this PropertyInfo propertyInfo) => + propertyInfo.FirstAttribute(true); - public static IEnumerable? MultipleAttribute(this PropertyInfo propertyInfo) - { - return propertyInfo.MultipleAttribute(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, bool inherit) - { - var attrs = propertyInfo.GetCustomAttributes(typeof(TAttribute), inherit); - return (attrs.Length > 0 ? attrs.ToList().ConvertAll(input => (TAttribute)input) : null); - } + public static IEnumerable? MultipleAttribute(this PropertyInfo propertyInfo) => + propertyInfo.MultipleAttribute(true); - /// - /// 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(); + 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; + } - return string.Concat(type.FullName, ", ", - assemblyName.FullName.StartsWith("App_Code.") ? "App_Code" : assemblyName.Name); - } + /// + /// 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(); - /// - /// 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 + return string.Concat(type.FullName, ", ", assemblyName.FullName.StartsWith("App_Code.") ? "App_Code" : assemblyName.Name); + } - if (type.IsGenericTypeDefinition == false) - return type.IsAssignableFrom(c); + /// + /// 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) + 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) - { - 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]; + return c.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == type); + } - // otherwise is not an 'enumerated' 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) + { 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)); - - // 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 + { + return Attempt.Succeed( + type.InvokeMember( + memberAlias, + BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public, + null, + target, + null)); + } + catch (MissingMethodException ex) + { + return Attempt.Fail(ex); + } + }; - return attempt; + // 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)); + + // 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 8928d221c558..1ea73af00910 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 70dd11ff3364..1ad94cbdc3e3 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) - { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.DocumentType, entity.Key).EnsureClosed(); - } + 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 IMediaType entity) + /// + /// 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.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.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.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.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.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.MemberType, 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 IMemberGroup 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) + return new GuidUdi(Constants.UdiEntityType.MemberGroup, 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) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.DataType, 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 EntityContainer entity) + string type; + if (entity is IContentType) + { + type = Constants.UdiEntityType.DocumentType; + } + else if (entity is IMediaType) { - if (entity == null) throw new ArgumentNullException("entity"); + 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)); + } - 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(); + 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 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)); + } + + 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 IMember entity) + /// + /// 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"); + 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 GuidUdi(Constants.UdiEntityType.DictionaryItem, entity.Key).EnsureClosed(); + } - 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 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 IContentBase 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"); + 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(); + } - 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 IContentBase 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) + string type; + if (entity is IContent) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.RelationType, entity.Key).EnsureClosed(); + 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)); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static StringUdi GetUdi(this ILanguage entity) + 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) { - 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) + 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) { - if (entity == null) throw new ArgumentNullException("entity"); + throw new ArgumentNullException("entity"); + } - // entity could eg be anything implementing IThing - // so we have to go through casts here + return new StringUdi(Constants.UdiEntityType.Language, entity.IsoCode).EnsureClosed(); + } - var template = entity as ITemplate; - if (template != null) return template.GetUdi(); + /// + /// 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"); + } - var contentType = entity as IContentType; - if (contentType != null) return contentType.GetUdi(); + // entity could eg be anything implementing IThing + // so we have to go through casts here + if (entity is ITemplate template) + { + return template.GetUdi(); + } - var mediaType = entity as IMediaType; - if (mediaType != null) return mediaType.GetUdi(); + if (entity is IContentType contentType) + { + return contentType.GetUdi(); + } - var memberType = entity as IMemberType; - if (memberType != null) return memberType.GetUdi(); + if (entity is IMediaType mediaType) + { + return mediaType.GetUdi(); + } - var memberGroup = entity as IMemberGroup; - if (memberGroup != null) return memberGroup.GetUdi(); + if (entity is IMemberType memberType) + { + return memberType.GetUdi(); + } - var contentTypeComposition = entity as IContentTypeComposition; - if (contentTypeComposition != null) return contentTypeComposition.GetUdi(); + if (entity is IMemberGroup memberGroup) + { + return memberGroup.GetUdi(); + } - var dataTypeComposition = entity as IDataType; - if (dataTypeComposition != null) return dataTypeComposition.GetUdi(); + if (entity is IContentTypeComposition contentTypeComposition) + { + return contentTypeComposition.GetUdi(); + } - var container = entity as EntityContainer; - if (container != null) return container.GetUdi(); + if (entity is IDataType dataTypeComposition) + { + return dataTypeComposition.GetUdi(); + } - var media = entity as IMedia; - if (media != null) return media.GetUdi(); + if (entity is EntityContainer container) + { + return container.GetUdi(); + } - var content = entity as IContent; - if (content != null) return content.GetUdi(); + if (entity is IMedia media) + { + return media.GetUdi(); + } - var member = entity as IMember; - if (member != null) return member.GetUdi(); + if (entity is IContent content) + { + return content.GetUdi(); + } - var stylesheet = entity as Stylesheet; - if (stylesheet != null) return stylesheet.GetUdi(); + if (entity is IMember member) + { + return member.GetUdi(); + } - var script = entity as Script; - if (script != null) return script.GetUdi(); + if (entity is Stylesheet stylesheet) + { + return stylesheet.GetUdi(); + } - var dictionaryItem = entity as IDictionaryItem; - if (dictionaryItem != null) return dictionaryItem.GetUdi(); + if (entity is Script script) + { + return script.GetUdi(); + } - var macro = entity as IMacro; - if (macro != null) return macro.GetUdi(); + if (entity is IDictionaryItem dictionaryItem) + { + return dictionaryItem.GetUdi(); + } - var partialView = entity as IPartialView; - if (partialView != null) return partialView.GetUdi(); + if (entity is IMacro macro) + { + return macro.GetUdi(); + } - var contentBase = entity as IContentBase; - if (contentBase != null) return contentBase.GetUdi(); + if (entity is IPartialView partialView) + { + return partialView.GetUdi(); + } - var relationType = entity as IRelationType; - if (relationType != null) return relationType.GetUdi(); + if (entity is IContentBase contentBase) + { + return contentBase.GetUdi(); + } - var language = entity as ILanguage; - if (language != null) return language.GetUdi(); + if (entity is IRelationType relationType) + { + return relationType.GetUdi(); + } - throw new NotSupportedException(string.Format("Entity type {0} is not supported.", entity.GetType().FullName)); + 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 5b4e3a92d9ce..53e86109c3f6 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) - { - var interfaceTypes = givenType.GetInterfaces(); - - foreach (var it in interfaceTypes) - { - if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType) - { - return true; - } - } + private static bool IsAssignableToGenericType(this Type givenType, Type genericType) + { + Type[] interfaceTypes = givenType.GetInterfaces(); - if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) + foreach (Type it in interfaceTypes) + { + 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 794c206db8dd..b0256ad9e625 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 7d0e31f285a5..e5ec62530d66 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 52adbc6b67da..60ef7b6a7eb5 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) + 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) { - 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); + throw new ArgumentException("Path must start with a slash.", "path"); } - /// - /// 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 (query.Length > 0 && query.StartsWith("?") == false) { - 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; + throw new ArgumentException("Query must start with a question mark.", "query"); } - /// - /// 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) + if (query == "?") { - return System.Web.HttpUtility.UrlDecode(uri.AbsolutePath); + query = string.Empty; } - /// - /// 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 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 System.Net.WebUtility.UrlDecode(uri.GetSafeAbsolutePath()); + return uri.AbsolutePath; } - /// - /// 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) + // 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) { - var path = uri.GetSafeAbsolutePath(); - if (uri.IsAbsoluteUri) + if (path != "/" && path.EndsWith("/") == false) { - if (path != "/" && path.EndsWith("/") == false) - uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path + "/" + uri.Query); - return uri; + uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path + "/" + uri.Query); } - 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) + if (path != "/" && path.EndsWith("/") == false) { - var path = uri.GetSafeAbsolutePath(); - if (uri.IsAbsoluteUri) + 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 != "/") { - if (path != "/") - uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path.TrimEnd(Constants.CharArrays.ForwardSlash) + uri.Query); + uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path.TrimEnd(Constants.CharArrays.ForwardSlash) + + uri.Query); } - else + } + else + { + if (path != "/") { - if (path != "/") - uri = new Uri(path.TrimEnd(Constants.CharArrays.ForwardSlash) + uri.Query, UriKind.Relative); + 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()); - } + return uri; + } - static string? GetSafeQuery(this Uri 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) { - if (uri.IsAbsoluteUri) - return uri.Query; + throw new ArgumentException("Uri is already absolute.", "uri"); + } - // 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 new Uri(baseUri.GetLeftPart(UriPartial.Authority) + uri.GetSafeAbsolutePath() + uri.GetSafeQuery()); + } - return query; - } + /// + /// 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)); - /// - /// Removes the port from the uri. - /// - /// The uri. - /// The same uri, without its port. - public static Uri WithoutPort(this Uri uri) + private static string? GetSafeQuery(this Uri uri) + { + if (uri.IsAbsoluteUri) { - return new Uri(uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped)); + return uri.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) - { - return new UriBuilder(uri) { Host = host }.Uri; - } + // 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 24326ef32740..4e9309da4586 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); + return new Version(semVersion.Major, semVersion.Minor, semVersion.Patch, build); + } - if (maxParts >= 4) - { - return new Version(semVersion.Major, semVersion.Minor, semVersion.Patch, build); - } - if (maxParts == 3) + if (maxParts == 3) + { + return new Version(semVersion.Major, semVersion.Minor, semVersion.Patch); + } + + 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) { - return new Version(semVersion.Major, semVersion.Minor, semVersion.Patch); + break; } - - return new Version(semVersion.Major, semVersion.Minor); } - public static Version SubtractRevision(this Version version) + for (var index = 0; index < parts.Count; index++) { - var parts = new List(new[] {version.Major, version.Minor, version.Build, version.Revision}); - - //remove all prefixed zero parts - while (parts[0] <= 0) + var part = parts[index]; + if (part <= 0) { - parts.RemoveAt(0); - if (parts.Count == 0) break; + parts.RemoveAt(index); + index++; } - - for (int index = 0; index < parts.Count; index++) + else { - var part = parts[index]; - if (part <= 0) - { - parts.RemoveAt(index); - index++; - } - else - { - //break when there isn't a zero part - break; - } + // break when there isn't a zero part + break; } + } - 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; + if (parts.Count == 0) + { + throw new InvalidOperationException("Cannot subtract a revision from a zero version"); + } - //the last non zero is actually the revision so we can just return - if (lastNonZero == (parts.Count -1)) - { - return FromList(parts); - } + var lastNonZero = parts.FindLastIndex(i => i > 0); - //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; - } + // 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 5cb763949766..b0058dd79812 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) => - { - //TODO: We aren't checking if this is timed out - - tcs.SetResult(null); + RegisteredWaitHandle? callbackHandle = 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); - } + // ReSharper disable once RedundantAssignment + callbackHandle = ThreadPool.RegisterWaitForSingleObject( + handle, + (state, timedOut) => + { + // TODO: We aren't checking if this is timed out + tcs.SetResult(null); - return tcs.Task; + // 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; } } diff --git a/src/Umbraco.Core/Extensions/XmlExtensions.cs b/src/Umbraco.Core/Extensions/XmlExtensions.cs index 141f4a0c1958..34e2b7b2aa4d 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 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); + + /// + /// 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) + { + 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); + } + /// - /// 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, params XPathVariable[]? variables) { - public static bool HasAttribute(this XmlAttributeCollection attributes, string attributeName) + 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); + nodeReader.MoveToContent(); + return XDocument.Load(nodeReader); } + } - /// - /// 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) + ///// + ///// Converts from an XElement to an XmlElement + ///// + ///// + ///// + public static XmlNode? ToXmlElement(this XContainer xElement) + { + var xmlDocument = new XmlDocument(); + using (XmlReader xmlReader = xElement.CreateReader()) { - var av = variables == null ? null : variables.ToArray(); - return SelectSingleNode(source, expression, av); + 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, params XPathVariable[]? variables) - { - if (variables == null || variables.Length == 0 || variables[0] == null) - return source.SelectSingleNode(expression); + return xmlDocument.DocumentElement; + } - return SelectNodes(source, expression, variables)?.Cast().FirstOrDefault(); + /// + /// 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); } + } - /// - /// 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) + public static T? RequiredAttributeValue(this XElement xml, string attributeName) + { + if (xml == null) { - if (variables == null || variables.Length == 0 || variables[0] == null) - return source.SelectSingleNode(expression); - - return SelectNodes(source, expression, variables).Cast().FirstOrDefault(); + throw new ArgumentNullException(nameof(xml)); } - /// - /// Converts from an XDocument to an XmlDocument - /// - /// - /// - public static XmlDocument ToXmlDocument(this XDocument xDocument) + if (xml.HasAttributes == false) { - var xmlDocument = new XmlDocument(); - using (var xmlReader = xDocument.CreateReader()) - { - xmlDocument.Load(xmlReader); - } - return xmlDocument; + throw new InvalidOperationException($"{attributeName} not found in xml"); } - /// - /// Converts from an XmlDocument to an XDocument - /// - /// - /// - public static XDocument ToXDocument(this XmlDocument xmlDocument) + XAttribute? attribute = xml.Attribute(attributeName); + if (attribute is null) { - using (var nodeReader = new XmlNodeReader(xmlDocument)) - { - nodeReader.MoveToContent(); - return XDocument.Load(nodeReader); - } + throw new InvalidOperationException($"{attributeName} not found in xml"); } - ///// - ///// Converts from an XElement to an XmlElement - ///// - ///// - ///// - public static XmlNode? ToXmlElement(this XContainer xElement) + Attempt result = attribute.Value.TryConvertTo(); + if (result.Success) { - var xmlDocument = new XmlDocument(); - using (var xmlReader = xElement.CreateReader()) - { - xmlDocument.Load(xmlReader); - } - return xmlDocument.DocumentElement; + return result.Result; } - /// - /// Converts from an XmlElement to an XElement - /// - /// - /// - public static XElement ToXElement(this XmlNode xmlElement) + 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) { - using (var nodeReader = new XmlNodeReader(xmlElement)) - { - nodeReader.MoveToContent(); - return XElement.Load(nodeReader); - } + throw new ArgumentNullException("xml"); } - public static T? RequiredAttributeValue(this XElement xml, string attributeName) + if (xml.HasAttributes == false) { - 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)}"); + return default; } - public static T? AttributeValue(this XElement xml, string attributeName) + if (xml.Attribute(attributeName) == null) { - if (xml == null) throw new ArgumentNullException("xml"); - if (xml.HasAttributes == false) return default(T); + return default; + } - if (xml.Attribute(attributeName) == null) - return default(T); + var val = xml.Attribute(attributeName)?.Value; + Attempt result = val.TryConvertTo(); + if (result.Success) + { + return result.Result; + } - var val = xml.Attribute(attributeName)?.Value; - var result = val.TryConvertTo(); - if (result.Success) - return result.Result; + return default; + } - return default(T); + public static T? AttributeValue(this XmlNode xml, string attributeName) + { + if (xml == null) + { + throw new ArgumentNullException("xml"); } - public static T? AttributeValue(this XmlNode xml, string attributeName) + if (xml.Attributes == null) { - 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; + } - return default(T); + if (xml.Attributes[attributeName] == null) + { + return default; } - public static XElement? GetXElement(this XmlNode node) + var val = xml.Attributes[attributeName]?.Value; + Attempt result = val.TryConvertTo(); + if (result.Success) { - XDocument xDoc = new XDocument(); - using (XmlWriter xmlWriter = xDoc.CreateWriter()) - node.WriteTo(xmlWriter); - return xDoc.Root; + return result.Result; } - public static XmlNode? GetXmlNode(this XContainer element) + return default; + } + + public static XElement? GetXElement(this XmlNode node) + { + var xDoc = new XDocument(); + using (XmlWriter xmlWriter = xDoc.CreateWriter()) { - using (var xmlReader = element.CreateReader()) - { - var xmlDoc = new XmlDocument(); - xmlDoc.Load(xmlReader); - return xmlDoc.DocumentElement; - } + node.WriteTo(xmlWriter); } - public static XmlNode? GetXmlNode(this XContainer element, XmlDocument xmlDoc) + return xDoc.Root; + } + + public static XmlNode? GetXmlNode(this XContainer element) + { + using (XmlReader xmlReader = element.CreateReader()) { - var node = element.GetXmlNode(); - if (node is not null) - { - return xmlDoc.ImportNode(node, true); - } + var xmlDoc = new XmlDocument(); + xmlDoc.Load(xmlReader); + return xmlDoc.DocumentElement; + } + } - return null; + public static XmlNode? GetXmlNode(this XContainer element, XmlDocument xmlDoc) + { + XmlNode? node = element.GetXmlNode(); + if (node is not null) + { + return xmlDoc.ImportNode(node, true); } - // 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) + 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 { - 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(); + 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 e572818baf1b..e7f9eeb83d46 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(); - } - - /// - /// Gets the disabled controllers. - /// - public TypeList Controllers { get; } + public DisabledFeatures() => Controllers = new TypeList(); - /// - /// Disables the device preview feature of previewing. - /// - public bool DisableDevicePreview { get; set; } + /// + /// Gets the disabled controllers. + /// + public TypeList Controllers { get; } - /// - /// If true, all references to templates will be removed in the back office and routing - /// - public bool DisableTemplates { 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; } } diff --git a/src/Umbraco.Core/Features/EnabledFeatures.cs b/src/Umbraco.Core/Features/EnabledFeatures.cs index 5fb7a581dc93..aee19e2f14b0 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 efb5337a0058..8beaeef32148 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 5b6bfd7bfb9c..0f971d8ba17a 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(); - } + Disabled = new DisabledFeatures(); + Enabled = new EnabledFeatures(); + } - /// - /// Gets the disabled features. - /// - public DisabledFeatures Disabled { get; } + /// + /// Gets the disabled features. + /// + public DisabledFeatures Disabled { get; } - /// - /// Gets the enabled features. - /// - public EnabledFeatures Enabled { get; } + /// + /// Gets the enabled features. + /// + public EnabledFeatures Enabled { get; } - /// - /// Determines whether a controller is enabled. - /// - public bool IsControllerEnabled(Type? feature) + /// + /// Determines whether a controller is enabled. + /// + public bool IsControllerEnabled(Type? feature) + { + if (typeof(IUmbracoFeature).IsAssignableFrom(feature)) { - if (typeof(IUmbracoFeature).IsAssignableFrom(feature)) - return Disabled.Controllers.Contains(feature) == false; - - throw new NotSupportedException("Not a supported feature type."); + return Disabled.Controllers.Contains(feature) == false; } + + throw new NotSupportedException("Not a supported feature type."); } } diff --git a/src/Umbraco.Core/GuidUdi.cs b/src/Umbraco.Core/GuidUdi.cs index 53c495ba87a5..e8280bceb60c 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 guid part of the identifier. - /// - public Guid Guid { get; private set; } + /// 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 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")) + /// + /// Initializes a new instance of the GuidUdi class with an uri value. + /// + /// The uri value of the udi. + public GuidUdi(Uri uriValue) + : base(uriValue) + { + 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 Equals(object? obj) - { - var other = obj as GuidUdi; - if (other is null) return false; - return EntityType == other.EntityType && Guid == other.Guid; - } + /// + public override bool IsRoot => Guid == Guid.Empty; - public override int GetHashCode() + public override bool Equals(object? obj) + { + if (obj is not GuidUdi other) { - return base.GetHashCode(); + return false; } - /// - public override bool IsRoot - { - get { return Guid == Guid.Empty; } - } + return EntityType == other.EntityType && Guid == other.Guid; + } - public GuidUdi EnsureClosed() - { - EnsureNotRoot(); - return this; - } + public override int GetHashCode() => base.GetHashCode(); + + public GuidUdi EnsureClosed() + { + EnsureNotRoot(); + return this; } } diff --git a/src/Umbraco.Core/GuidUtils.cs b/src/Umbraco.Core/GuidUtils.cs index e6ccd6b27f1f..290f36cdcf2c 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 { + 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', + }; + /// - /// Utility methods for the struct. + /// 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. /// - public static class GuidUtils + /// The first guid. + /// The seconds guid. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid Combine(Guid a, Guid b) { - /// - /// 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); + var ad = new DecomposedGuid(a); + var bd = new DecomposedGuid(b); - ad.Hi ^= bd.Hi; - ad.Lo ^= bd.Lo; + ad.Hi ^= bd.Hi; + ad.Lo ^= bd.Lo; - return ad.Value; - } + return ad.Value; + } - /// - /// A decomposed guid. Allows access to the high and low bits without unsafe code. - /// - [StructLayout(LayoutKind.Explicit)] - private struct DecomposedGuid + /// + /// 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) { - [FieldOffset(00)] public Guid Value; - [FieldOffset(00)] public long Hi; - [FieldOffset(08)] public long Lo; - - public DecomposedGuid(Guid value) : this() => this.Value = value; + length = 26; } - 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) + 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) + { + break; + } - if (length <= 0 || length > 26) - length = 26; + chars[j++] = Base32Table[(bytes[i] & 0b1111_1000) >> 3]; + if (j == length) + { + break; + } - var bytes = guid.ToByteArray(); // a Guid is 128 bits ie 16 bytes + chars[j++] = Base32Table[((bytes[i] & 0b0000_0111) << 2) | ((bytes[i + 1] & 0b1100_0000) >> 6)]; + if (j == length) + { + break; + } - // this could be optimized by making it unsafe, - // and fixing the table + bytes + chars (see Convert.ToBase64CharArray) + chars[j++] = Base32Table[(bytes[i + 1] & 0b0011_1110) >> 1]; + if (j == length) + { + break; + } - // 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 + chars[j++] = Base32Table[(bytes[i + 1] & 0b0000_0001) | ((bytes[i + 2] & 0b1111_0000) >> 4)]; + if (j == length) + { + break; + } - // so it turns into a 3*8+2 = 26 chars string - var chars = new char[length]; + chars[j++] = Base32Table[((bytes[i + 2] & 0b0000_1111) << 1) | ((bytes[i + 3] & 0b1000_0000) >> 7)]; + if (j == length) + { + break; + } - var i = 0; - var j = 0; + chars[j++] = Base32Table[(bytes[i + 3] & 0b0111_1100) >> 2]; + if (j == length) + { + break; + } - while (i < 15) + chars[j++] = Base32Table[((bytes[i + 3] & 0b0000_0011) << 3) | ((bytes[i + 4] & 0b1110_0000) >> 5)]; + 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 + 4] & 0b0001_1111]; - return new string(chars); + 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 f15edfa1be86..28fbea027bff 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.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; - - public AuditNotificationsHandler( - IAuditService auditService, - IUserService userService, - IEntityService entityService, - IIpResolver ipResolver, - IOptionsMonitor globalSettings, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IMemberService memberService) - { - _auditService = auditService; - _userService = userService; - _entityService = entityService; - _ipResolver = ipResolver; - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _memberService = memberService; - _globalSettings = globalSettings.CurrentValue; - } + _auditService = auditService; + _userService = userService; + _entityService = entityService; + _ipResolver = ipResolver; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _memberService = memberService; + _globalSettings = globalSettings.CurrentValue; + } - private IUser CurrentPerformingUser + private IUser CurrentPerformingUser + { + get { - get - { - var identity = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; - var user = identity == null ? null : _userService.GetUserById(Convert.ToInt32(identity.Id)); - return user ?? UnknownUser(_globalSettings); - } + IUser? identity = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; + IUser? 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 PerformingIp => _ipResolver.GetCurrentRequestIpAddress(); - private string FormatEmail(IMember? member) => member == null ? string.Empty : member.Email.IsNullOrWhiteSpace() ? "" : $"<{member.Email}>"; + public static IUser UnknownUser(GlobalSettings globalSettings) => new User(globalSettings) + { + Id = Constants.Security.UnknownUserId, + Name = Constants.Security.UnknownUserName, + Email = string.Empty, + }; - private string FormatEmail(IUser user) => user == null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? "" : $"<{user.Email}>"; + 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) + { + 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/assigned", + $"roles modified, assigned {roles}"); + } + } - public void Handle(MemberSavedNotification notification) + public void Handle(AssignedUserGroupPermissionsNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable perms = notification.EntityPermissions; + foreach (EntityPermission perm in perms) { - var performingUser = CurrentPerformingUser; - var members = notification.SavedEntities; - foreach (var member in members) - { - var dp = string.Join(", ", ((Member)member).GetWereDirtyProperties()); + 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, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", - "umbraco/member/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}"); - } + _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) + public void Handle(MemberDeletedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable members = notification.DeletedEntities; + foreach (IMember member in members) { - 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)}"); - } + _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) + public void Handle(MemberSavedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable members = notification.SavedEntities; + foreach (IMember member in members) { - 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}"); - } + 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) + 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) { - 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}"); - } + 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(ExportedMemberNotification notification) + public void Handle(UserDeletedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable affectedUsers = notification.DeletedEntities; + foreach (IUser affectedUser in affectedUsers) { - var performingUser = CurrentPerformingUser; - var member = notification.Member; - - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + _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"); + affectedUser.Id, + $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", + "umbraco/user/delete", + "delete user"); } + } - public void Handle(UserSavedNotification notification) + public void Handle(UserGroupWithUsersSavedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + foreach (UserGroupWithUsers groupWithUser in notification.SavedEntities) { - var performingUser = CurrentPerformingUser; - var affectedUsers = notification.SavedEntities; - foreach (var affectedUser in affectedUsers) - { - var groups = affectedUser.WasPropertyDirty("Groups") - ? string.Join(", ", affectedUser.Groups.Select(x => x.Alias)) - : null; + IUserGroup group = groupWithUser.UserGroup; - var dp = string.Join(", ", ((User)affectedUser).GetWereDirtyProperties()); + 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; - _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)}"); + var sb = new StringBuilder(); + sb.Append($"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)};"); + if (sections != null) + { + 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}"); + sb.Append($"default perms: {perms}"); + } - // now audit the users that have changed + _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}"); - 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})"); - } + // now audit the users that have changed + foreach (IUser 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})"); - } + 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(AssignedUserGroupPermissionsNotification notification) + public void Handle(UserSavedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable affectedUsers = notification.SavedEntities; + foreach (IUser affectedUser in affectedUsers) { - var performingUser = CurrentPerformingUser; - var perms = notification.EntityPermissions; - foreach (EntityPermission perm in perms) - { - var group = _userService.GetUserGroupById(perm.UserGroupId); - var assigned = string.Join(", ", perm.AssignedPermissions ?? Array.Empty()); - var entity = _entityService.Get(perm.EntityId); + var groups = affectedUser.WasPropertyDirty("Groups") + ? string.Join(", ", affectedUser.Groups.Select(x => x.Alias)) + : null; - _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}\""); - } + 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 466e09e3f10d..d441509a8571 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; + private readonly IPublicAccessService _publicAccessService; - public PublicAccessHandler(IPublicAccessService publicAccessService) => - _publicAccessService = publicAccessService ?? throw new ArgumentNullException(nameof(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); - public void Handle(MemberGroupDeletedNotification notification) => Handle(notification.DeletedEntities); + public void Handle(MemberGroupSavedNotification notification) => Handle(notification.SavedEntities); - private void Handle(IEnumerable affectedEntities) + 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 d8c1ac2a0702..3506d335b887 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; + private long _combinedHash = 5381L; - public void AddInt(int i) - { - _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; - } + public void AddInt(int i) => _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; + + public void AddObject(object o) => AddInt(o.GetHashCode()); - public void AddObject(object o) + public void AddDateTime(DateTime d) => AddInt(d.GetHashCode()); + + public void AddString(string s) + { + if (s != null) { - AddInt(o.GetHashCode()); + AddInt(StringComparer.InvariantCulture.GetHashCode(s)); } + } - public void AddDateTime(DateTime d) + public void AddCaseInsensitiveString(string s) + { + if (s != null) { - AddInt(d.GetHashCode()); + AddInt(StringComparer.InvariantCultureIgnoreCase.GetHashCode(s)); } + } - public void AddString(string s) + public void AddFileSystemItem(FileSystemInfo f) + { + // if it doesn't exist, don't proceed. + if (!f.Exists) { - if (s != null) - AddInt((StringComparer.InvariantCulture).GetHashCode(s)); + return; } - public void AddCaseInsensitiveString(string s) + AddCaseInsensitiveString(f.FullName); + AddDateTime(f.CreationTimeUtc); + AddDateTime(f.LastWriteTimeUtc); + + // check if it is a file or folder + if (f is FileInfo fileInfo) { - if (s != null) - AddInt((StringComparer.InvariantCultureIgnoreCase).GetHashCode(s)); + AddInt(fileInfo.Length.GetHashCode()); } - public void AddFileSystemItem(FileSystemInfo f) + if (f is DirectoryInfo dirInfo) { - //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) + foreach (FileInfo d in dirInfo.GetFiles()) { - AddInt(fileInfo.Length.GetHashCode()); + AddFile(d); } - var dirInfo = f as DirectoryInfo; - if (dirInfo != null) + foreach (DirectoryInfo s in dirInfo.GetDirectories()) { - foreach (var d in dirInfo.GetFiles()) - { - AddFile(d); - } - foreach (var s in dirInfo.GetDirectories()) - { - AddFolder(s); - } + AddFolder(s); } } + } - public void AddFile(FileInfo f) - { - AddFileSystemItem(f); - } + public void AddFile(FileInfo f) => AddFileSystemItem(f); - public void AddFolder(DirectoryInfo d) - { - AddFileSystemItem(d); - } + 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); - } - - } + /// + /// 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 6d98ec57b85b..ecf209c53221 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; + + hash = (31 * hash) + item.GetHashCode(); } + + return hash; } + } - public static int GetHashCode(IEnumerable list) + 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; + + 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) + /// + /// 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(); + + 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) + /// + /// 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 { - unchecked - { - return 31 * hashCode + arg!.GetHashCode(); - } + return (31 * hashCode) + arg!.GetHashCode(); } } } diff --git a/src/Umbraco.Core/HashGenerator.cs b/src/Umbraco.Core/HashGenerator.cs index 944e0bdf4997..cad3d4b6b8fe 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 - { - public HashGenerator() - { - _writer = new StreamWriter(_ms, Encoding.Unicode, 1024, leaveOpen: true); - } + private readonly MemoryStream _ms = new(); + private StreamWriter _writer; - private readonly MemoryStream _ms = new MemoryStream(); - private StreamWriter _writer; + public HashGenerator() => _writer = new StreamWriter(_ms, Encoding.Unicode, 1024, true); - public void AddInt(int i) - { - _writer.Write(i); - } + public void AddInt(int i) => _writer.Write(i); - public void AddLong(long i) - { - _writer.Write(i); - } + public void AddLong(long i) => _writer.Write(i); - public void AddObject(object o) + 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(o); + _writer.Write(s); } + } - public void AddDateTime(DateTime d) + 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(d.Ticks); + _writer.Write(s.ToUpperInvariant()); } + } - public void AddString(string s) + public void AddFileSystemItem(FileSystemInfo f) + { + // if it doesn't exist, don't proceed. + if (f.Exists == false) { - if (s != null) - _writer.Write(s); + return; } - public void AddCaseInsensitiveString(string s) + AddCaseInsensitiveString(f.FullName); + AddDateTime(f.CreationTimeUtc); + AddDateTime(f.LastWriteTimeUtc); + + // check if it is a file or folder + if (f is FileInfo fileInfo) { - //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()); + AddLong(fileInfo.Length); } - public void AddFileSystemItem(FileSystemInfo f) + if (f is DirectoryInfo dirInfo) { - //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) + foreach (FileInfo d in dirInfo.GetFiles()) { - AddLong(fileInfo.Length); + AddFile(d); } - if (f is DirectoryInfo dirInfo) + foreach (DirectoryInfo s in dirInfo.GetDirectories()) { - foreach (var d in dirInfo.GetFiles()) - { - AddFile(d); - } - foreach (var s in dirInfo.GetDirectories()) - { - AddFolder(s); - } + AddFolder(s); } } + } - public void AddFile(FileInfo f) - { - AddFileSystemItem(f); - } + 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); - public void AddFolder(DirectoryInfo d) + 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) { - AddFileSystemItem(d); + throw new InvalidOperationException("No hashing type found by name " + hashType); } - /// - /// Returns the generated hash output of all added objects - /// - /// - public string GenerateHash() + using (hasher) { - //flush,close,dispose the writer,then create a new one since it's possible to keep adding after GenerateHash is called. + var buffer = _ms.GetBuffer(); - _writer.Flush(); - _writer.Close(); - _writer.Dispose(); - _writer = new StreamWriter(_ms, Encoding.UTF8, 1024, leaveOpen: true); + // get the hashed values created by our selected provider + var hashedByteArray = hasher.ComputeHash(buffer); - var hashType = CryptoConfig.AllowOnlyFipsAlgorithms ? "SHA1" : "MD5"; + // create a StringBuilder object + var stringBuilder = new StringBuilder(); - //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) + // loop to each byte + foreach (var b in hashedByteArray) { - 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(); + // append it to our StringBuilder + stringBuilder.Append(b.ToString("x2")); } - } - protected override void DisposeResources() - { - _writer.Close(); - _writer.Dispose(); - _ms.Close(); - _ms.Dispose(); + // 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 93cdea7c0b1f..42420b895432 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 7123255b0d9c..4dddf34270f5 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. + /// + 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 class AbstractSettingsCheck : HealthCheck + 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; - - /// - /// 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() + // 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)); + + 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 2ded5a0659bb..a212a69a3eb9 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; - } + _textService = textService; + _contentSettings = contentSettings; + } - /// - public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Configuration.MacroErrorsCheck; + /// + public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Configuration.MacroErrorsCheck; - /// - public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; + /// + public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; - /// - public override string ItemPath => Constants.Configuration.ConfigContentMacroErrors; + /// + public override string ItemPath => Constants.Configuration.ConfigContentMacroErrors; - /// - /// Gets the values to compare against. - /// - public override IEnumerable Values + /// + /// Gets the values to compare against. + /// + public override IEnumerable Values + { + get { - get + var values = new List { - var values = new List - { - new AcceptableConfiguration - { - IsRecommended = true, - Value = MacroErrorBehaviour.Inline.ToString() - }, - new AcceptableConfiguration - { - IsRecommended = false, - Value = MacroErrorBehaviour.Silent.ToString() - } - }; + new() { IsRecommended = true, Value = MacroErrorBehaviour.Inline.ToString() }, + new() { IsRecommended = false, Value = MacroErrorBehaviour.Silent.ToString() }, + }; - return values; - } + return values; } + } - /// - public override string CurrentValue => _contentSettings.CurrentValue.MacroErrors.ToString(); + /// + 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 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 }); - } + /// + /// 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 9cb56392056a..9629aa8917b1 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"; - - /// - /// Initializes a new instance of the class. - /// - 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 - { - 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; - } + new() { IsRecommended = false, Value = DefaultFromEmail }, new() { 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; } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Data/DatabaseIntegrityCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Data/DatabaseIntegrityCheck.cs index dda7fb2e6e57..4c3936f6cbde 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; + + /// + /// Initializes a new instance of the class. + /// + public DatabaseIntegrityCheck( + IContentService contentService, + IMediaService mediaService) + { + _contentService = contentService; + _mediaService = mediaService; + } + /// - /// Health check for the integrity of the data in the database. + /// Get the status for this health check /// - [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 override Task> GetStatus() => + Task.FromResult((IEnumerable)new[] { CheckDocuments(false), CheckMedia(false) }); + + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) { - 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"; - - /// - /// Initializes a new instance of the class. - /// - public DatabaseIntegrityCheck( - IContentService contentService, - IMediaService mediaService) + switch (action.Alias) { - _contentService = contentService; - _mediaService = mediaService; + case SFixContentPaths: + return CheckDocuments(true); + case SSsFixMediaPaths: + return CheckMedia(true); + default: + throw new InvalidOperationException("Action not supported"); } + } - /// - /// 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) + private static string GetReport(ContentDataIntegrityReport report, string entityType, bool detailed) + { + var sb = new StringBuilder(); + + if (report.Ok) { - ContentDataIntegrityReport report = doCheck(); + sb.AppendLine($"

All {entityType} paths are valid

"); - var actions = new List(); - if (!report.Ok) + if (!detailed) { - actions.Add(new HealthCheckAction(actionAlias, Id) - { - Name = actionName - }); + return sb.ToString(); } - - 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) + else { - 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.

"); - } + sb.AppendLine($"

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

"); + } - if (detailed && report.DetectedIssues.Count > 0) + if (detailed && report.DetectedIssues.Count > 0) + { + sb.AppendLine("
    "); + foreach (IGrouping> + issueGroup in report.DetectedIssues.GroupBy(x => x.Value.IssueType)) { - 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("
    "); + 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("
  • "); } - return sb.ToString(); + sb.AppendLine("
"); } - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + 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) { - switch (action.Alias) - { - case SFixContentPaths: - return CheckDocuments(true); - case SSsFixMediaPaths: - return CheckMedia(true); - default: - throw new InvalidOperationException("Action not supported"); - } + 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 d28c3ca8f54b..ee4d9fe78863 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; - - /// - /// Initializes a new instance of the class. - /// - 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 - { - 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"); - } + new() { 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"); } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Permissions/FolderAndFilePermissionsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Permissions/FolderAndFilePermissionsCheck.cs index d10dc8fedd6d..13a45c169c60 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) - { - _textService = textService; - _filePermissionHelper = filePermissionHelper; - } + /// + /// Get the status for this health check + /// + public override Task> GetStatus() + { + _filePermissionHelper.RunFilePermissionTestSuite( + out Dictionary> errors); - /// - /// Get the status for this health check - /// - public override Task> GetStatus() + return Task.FromResult(errors.Select(x => new HealthCheckStatus(GetMessage(x)) { - _filePermissionHelper.RunFilePermissionTestSuite(out Dictionary> errors); + 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"); - return Task.FromResult(errors.Select(x => new HealthCheckStatus(GetMessage(x)) - { - ResultType = x.Value.Any() ? StatusResultType.Error : StatusResultType.Success, - ReadMoreLink = GetReadMoreLink(x), - Description = GetErrorDescription(x) - })); + private string? GetErrorDescription(KeyValuePair> status) + { + if (!status.Value.Any()) + { + return null; } - private string? GetErrorDescription(KeyValuePair> status) + var sb = new StringBuilder("The following failed:"); + + sb.AppendLine("
    "); + foreach (var error in status.Value) { - if (!status.Value.Any()) - { - return null; - } + sb.Append("
  • " + error + "
  • "); + } - var sb = new StringBuilder("The following failed:"); + sb.AppendLine("
"); + return sb.ToString(); + } - sb.AppendLine("
    "); - foreach (var error in status.Value) - { - sb.Append("
  • " + error + "
  • "); - } + private string GetMessage(KeyValuePair> status) + => _textService.Localize("permissions", status.Key); - sb.AppendLine("
"); - return sb.ToString(); + private string? GetReadMoreLink(KeyValuePair> status) + { + if (!status.Value.Any()) + { + return null; } - private string GetMessage(KeyValuePair> status) - => _textService.Localize("permissions", status.Key); - - private string? GetReadMoreLink(KeyValuePair> status) + switch (status.Key) { - if (!status.Value.Any()) - { + 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; - } - - 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 d99f05d7388b..041ace503f42 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 daeea79f026c..5e830e1f61ae 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 { + 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) + { + } + /// - /// Provides a base class for health checks of http header values. + /// Initializes a new instance of the class. /// - public abstract class BaseHttpHeaderCheck : HealthCheck + protected BaseHttpHeaderCheck( + IHostingEnvironment hostingEnvironment, + ILocalizedTextService textService, + string header, + string localizedTextPrefix, + bool 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) - { + 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; } - [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; - } + /// + /// Gets a link to an external read more page. + /// + protected abstract string ReadMoreLink { get; } - 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; + private static HttpClient HttpClient => httpClient ??= new HttpClient(); - // Access the site home page and check for the click-jack protection header or meta tag - var url = _hostingEnvironment.ApplicationMainUrl?.GetLeftPart(UriPartial.Authority); + /// + /// Get the status for this health check + /// + public override async Task> GetStatus() => + await Task.WhenAll(CheckForHeader()); - try - { - using HttpResponseMessage response = await HttpClient.GetAsync(url); + /// + /// 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"); - // Check first for header - success = HasMatchingHeader(response.Headers.Select(x => x.Key)); + /// + /// The actual health check method. + /// + protected async Task CheckForHeader() + { + string message; + var success = false; - // If not found, and available, check for meta-tag - if (success == false && _metaTagOptionAvailable) - { - success = await DoMetaTagsContainKeyForHeader(response); - } + // Access the site home page and check for the click-jack protection header or meta tag + var url = _hostingEnvironment.ApplicationMainUrl?.GetLeftPart(UriPartial.Authority); - message = success - ? _textService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderFound") - : _textService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderNotFound"); - } - catch (Exception ex) + try + { + 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) { - 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); + return + new HealthCheckStatus(message) + { + ResultType = success ? StatusResultType.Success : StatusResultType.Error, + ReadMoreLink = success ? null : ReadMoreLink, + }; + } + + private static Dictionary ParseMetaTags(string html) + { + var regex = new Regex(" DoMetaTagsContainKeyForHeader(HttpResponseMessage response) + 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()) { - using (Stream stream = await response.Content.ReadAsStreamAsync()) + if (stream == null) { - if (stream == null) - { - return false; - } - - using (var reader = new StreamReader(stream)) - { - var html = reader.ReadToEnd(); - Dictionary metaTags = ParseMetaTags(html); - return HasMatchingHeader(metaTags.Keys); - } + return false; } - } - private static Dictionary ParseMetaTags(string html) - { - 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 8586989f32a8..2d15e49e6a70 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 99729286c56c..e211d7c25793 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) - { - _textService = textService; - _hostingEnvironment = hostingEnvironment; - } + private static HttpClient HttpClient => httpClient ??= new HttpClient(); - private static HttpClient HttpClient => s_httpClient ??= new HttpClient(); + /// + /// Get the status for this health check + /// + public override async Task> GetStatus() => + await Task.WhenAll(CheckForHeaders()); - /// - /// 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"); - /// - /// 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); - private async Task CheckForHeaders() + // Access the site home page and check for the headers + var request = new HttpRequestMessage(HttpMethod.Head, url); + try { - string message; - var success = false; - var url = _hostingEnvironment.ApplicationMainUrl?.GetLeftPart(UriPartial.Authority); + using HttpResponseMessage response = await HttpClient.SendAsync(request); - // Access the site home page and check for the headers - var request = new HttpRequestMessage(HttpMethod.Head, url); - try - { - 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" }; + 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) + // 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)) { - 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 7902f4e3f873..229999472e1a 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 0b58ca4b4060..dbff50c480b6 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 { - /// - /// 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 const int NumberOfDaysForExpiryWarning = 14; + private const string HttpPropertyKeyCertificateDaysToExpiry = "CertificateDaysToExpiry"; - private readonly ILocalizedTextService _textService; - private readonly IOptionsMonitor _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; + private static HttpClient? _httpClient; + private readonly IOptionsMonitor _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; - private static HttpClient? s_httpClient; + private readonly ILocalizedTextService _textService; - private static HttpClient HttpClient => s_httpClient ??= new HttpClient(new HttpClientHandler() - { - 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; + } - /// - /// Initializes a new instance of the class. - /// - /// The text service. - /// The global settings. - /// The hosting environment. - public HttpsCheck( - ILocalizedTextService textService, - IOptionsMonitor globalSettings, - IHostingEnvironment hostingEnvironment) + private static HttpClient _httpClientEnsureInitialized => _httpClient ??= new HttpClient(new HttpClientHandler + { + ServerCertificateCustomValidationCallback = ServerCertificateCustomValidation, + }); + + /// + public override async Task> GetStatus() => + await Task.WhenAll( + CheckIfCurrentSchemeIsHttps(), + CheckHttpsConfigurationSetting(), + CheckForValidCertificate()); + + /// + 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) { - _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) - { - if (certificate is not null) - { - requestMessage.Properties[HttpPropertyKeyCertificateDaysToExpiry] = (int)Math.Floor((certificate.NotAfter - DateTime.Now).TotalDays); - } + // 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; - return sslErrors == SslPolicyErrors.None; - } + var request = new HttpRequestMessage(HttpMethod.Head, url); - private async Task CheckForValidCertificate() + try { - 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; + using HttpResponseMessage response = await _httpClientEnsureInitialized.SendAsync(request); - var request = new HttpRequestMessage(HttpMethod.Head, url); - - try + if (response.StatusCode == HttpStatusCode.OK) { - 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" })) + else { - ResultType = success ? StatusResultType.Success : StatusResultType.Error, - ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckIfCurrentSchemeIsHttps - }); + result = StatusResultType.Error; + message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, response.ReasonPhrase }); + } } - - private Task CheckHttpsConfigurationSetting() + catch (Exception ex) { - bool httpsSettingEnabled = _globalSettings.CurrentValue.UseHttps; - Uri uri = _hostingEnvironment.ApplicationMainUrl; - - string resultMessage; - StatusResultType resultType; - if (uri.Scheme != Uri.UriSchemeHttps) + if (ex is WebException exception) { - resultMessage = _textService.Localize("healthcheck","httpsCheckConfigurationRectifyNotPossible"); - resultType = StatusResultType.Info; + message = exception.Status == WebExceptionStatus.TrustFailure + ? _textService.Localize("healthcheck", "httpsCheckInvalidCertificate", new[] { exception.Message }) + : _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, exception.Message }); } else { - resultMessage = _textService.Localize("healthcheck","httpsCheckConfigurationCheckResult", new[] { httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not" }); - resultType = httpsSettingEnabled ? StatusResultType.Success : StatusResultType.Error; + message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, ex.Message }); } - return Task.FromResult(new HealthCheckStatus(resultMessage) + 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 = resultType, - ReadMoreLink = resultType == StatusResultType.Success + ResultType = success ? StatusResultType.Success : StatusResultType.Error, + ReadMoreLink = success ? null - : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckHttpsConfigurationSetting + : 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 78ee2c0e124f..b36201d5aa7d 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 44b10ba0e3c2..55406b9c0afe 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 -{ - [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; +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; - public UmbracoApplicationUrlCheck(ILocalizedTextService textService, IOptionsMonitor webRoutingSettings) - { - _textService = textService; - _webRoutingSettings = webRoutingSettings; - } +[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; - /// - /// Executes the action and returns its status - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => throw new InvalidOperationException("UmbracoApplicationUrlCheck has no executable actions"); + public UmbracoApplicationUrlCheck( + ILocalizedTextService textService, + IOptionsMonitor webRoutingSettings) + { + _textService = textService; + _webRoutingSettings = webRoutingSettings; + } - /// - /// Get the status for this health check - /// - public override Task> GetStatus() => - Task.FromResult(CheckUmbracoApplicationUrl().Yield()); + /// + /// Executes the action and returns its status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => + throw new InvalidOperationException("UmbracoApplicationUrlCheck has no executable actions"); - private HealthCheckStatus CheckUmbracoApplicationUrl() - { - var url = _webRoutingSettings.CurrentValue.UmbracoApplicationUrl; + /// + /// Get the status for this health check + /// + public override Task> GetStatus() => + Task.FromResult(CheckUmbracoApplicationUrl().Yield()); - string resultMessage; - StatusResultType resultType; - var success = false; + private HealthCheckStatus CheckUmbracoApplicationUrl() + { + var url = _webRoutingSettings.CurrentValue.UmbracoApplicationUrl; - if (url.IsNullOrWhiteSpace()) - { - resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultFalse"); - resultType = StatusResultType.Warning; - } - else - { - resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultTrue", new[] { url }); - resultType = StatusResultType.Success; - success = true; - } + string resultMessage; + StatusResultType resultType; + var success = false; - return new HealthCheckStatus(resultMessage) - { - ResultType = resultType, - ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.UmbracoApplicationUrlCheck - }; + 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, + }; } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs index 570ca8002d74..ca988fe45af2 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 302a5829f6ea..6119f4c71570 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; - - /// - /// Initializes a new instance of the class. - /// - public SmtpCheck(ILocalizedTextService textService, IOptionsMonitor globalSettings) - { - _textService = textService; - _globalSettings = globalSettings; - } + _textService = textService; + _globalSettings = globalSettings; + } - /// - /// Get the status for this health check - /// - public override Task> GetStatus() => - Task.FromResult(CheckSmtpSettings().Yield()); + /// + /// 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"); + /// + /// 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() + private static bool CanMakeSmtpConnection(string host, int port) + { + try { - 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)) - { - message = _textService.Localize("healthcheck", "smtpMailSettingsHostNotConfigured"); - } - else + client.Connect(host, port); + using (NetworkStream stream = client.GetStream()) { - success = CanMakeSmtpConnection(smtpSettings.Host, smtpSettings.Port); - message = success - ? _textService.Localize("healthcheck", "smtpMailSettingsConnectionSuccess") - : _textService.Localize( - "healthcheck", "smtpMailSettingsConnectionFail", - new[] { smtpSettings.Host, smtpSettings.Port.ToString() }); + using (var writer = new StreamWriter(stream)) + using (var reader = new StreamReader(stream)) + { + writer.WriteLine("EHLO " + host); + writer.Flush(); + reader.ReadLine(); + return true; + } } } - - return - new HealthCheckStatus(message) - { - ResultType = success ? StatusResultType.Success : StatusResultType.Error, - ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.SmtpCheck - }; } + catch + { + return false; + } + } + + private HealthCheckStatus CheckSmtpSettings() + { + var success = false; + + SmtpSettings? smtpSettings = _globalSettings.CurrentValue.Smtp; - private static bool CanMakeSmtpConnection(string host, int port) + string message; + if (smtpSettings == null) { - try + message = _textService.Localize("healthcheck", "smtpMailSettingsNotFound"); + } + else + { + if (string.IsNullOrEmpty(smtpSettings.Host)) { - 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)) - { - writer.WriteLine("EHLO " + host); - writer.Flush(); - reader.ReadLine(); - return true; - } - } - } + message = _textService.Localize("healthcheck", "smtpMailSettingsHostNotConfigured"); } - catch + else { - return false; + 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 a5d3ae82dac2..564bcc59a557 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 59d6f912fad7..06a1bd27f3b0 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 +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 { - /// - /// 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() { - protected HealthCheck() + Type thisType = GetType(); + HealthCheckAttribute? meta = thisType.GetCustomAttribute(false); + if (meta == null) { - var thisType = GetType(); - var meta = thisType.GetCustomAttribute(false); - if (meta == null) - { - throw new InvalidOperationException($"The health check {thisType} requires a {typeof(HealthCheckAttribute)}"); - } - - Name = meta.Name; - Description = meta.Description; - Group = meta.Group; - Id = meta.Id; + 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 06bc05f44afe..7593a54cc2cd 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; - } + /// + /// 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 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; } + /// + /// 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; } + /// + /// 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; } - } + 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; } + /// + /// 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 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, 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; } + /// + /// 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; } - } + /// + /// 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 0fa66479714e..718a689caff6 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; - } + Id = new Guid(id); + Name = name; + } - public string Name { get; private set; } - public string? Description { get; set; } + public string Name { get; } - public string? Group { get; set; } + public string? Description { get; set; } - public Guid Id { get; private set; } + public string? Group { get; set; } - // TODO: Do we need more metadata? - } + 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 bcbee9036bc0..c2c47c194863 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 aee97647d98c..ae67c192f5d0 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 +namespace Umbraco.Cms.Core.HealthChecks; + +[DataContract(Name = "healthCheckGroup", Namespace = "")] +public class HealthCheckGroup { - [DataContract(Name = "healthCheckGroup", Namespace = "")] - public class HealthCheckGroup - { - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "checks")] - public List? Checks { 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 6dd6df4b8bf9..128e6dabbe37 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 af964857d88e..1d681690db66 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 48f2629e2a1e..375ddc7e2e6b 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 cba8ab5c0f03..1e7ea9053217 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 bde90627c775..afeb8ba9fa3b 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); + // find out if all checks pass or not + var allChecksSuccessful = true; + foreach (KeyValuePair> result in results) + { + var checkIsSuccess = result.Value.All(x => + x.ResultType == StatusResultType.Success || x.ResultType == StatusResultType.Info || + x.ResultType == StatusResultType.Warning); + if (checkIsSuccess == false) + { + allChecksSuccessful = false; + break; + } } - public void LogResults() + return new HealthCheckResults(results, allChecksSuccessful); + } + + public void LogResults() + { + Logger.LogInformation("Scheduled health check results:"); + foreach (KeyValuePair> result in ResultsAsDictionary) { - Logger.LogInformation("Scheduled health check results:"); - 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); - if (checkIsSuccess) - { - Logger.LogInformation("Checks for '{HealthCheckName}' all completed successfully.", checkName); - } - else - { - Logger.LogWarning("Checks for '{HealthCheckName}' completed with errors.", checkName); - } + 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); - } + foreach (HealthCheckStatus checkResult in checkResults) + { + Logger.LogInformation( + "Result for {HealthCheckName}: {HealthCheckResult}, Message: '{HealthCheckMessage}'", + checkName, + checkResult.ResultType, + checkResult.Message); } } + } - public string ResultsAsMarkDown(HealthCheckNotificationVerbosity verbosity) + public string ResultsAsMarkDown(HealthCheckNotificationVerbosity verbosity) + { + var newItem = "- "; + + var sb = new StringBuilder(); + + foreach (KeyValuePair> result in ResultsAsDictionary) { - var newItem = "- "; + var checkName = result.Key; + IEnumerable checkResults = result.Value; + var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success); - var sb = new StringBuilder(); + // add a new line if not the first check + if (result.Equals(ResultsAsDictionary.First()) == false) + { + sb.Append(Environment.NewLine); + } - foreach (var result in _results) + if (checkIsSuccess) + { + sb.AppendFormat("{0}Checks for '{1}' all completed successfully.{2}", newItem, checkName, Environment.NewLine); + } + else { - var checkName = result.Key; - var checkResults = result.Value; - var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success); + sb.AppendFormat("{0}Checks for '{1}' completed with errors.{2}", newItem, checkName, Environment.NewLine); + } - // add a new line if not the first check - if (result.Equals(_results.First()) == false) - { - sb.Append(Environment.NewLine); - } + foreach (HealthCheckStatus checkResult in checkResults) + { + sb.AppendFormat("\t{0}Result: '{1}'", newItem, checkResult.ResultType); - if (checkIsSuccess) + // With summary logging, only record details of warnings or errors + if (checkResult.ResultType != StatusResultType.Success || + verbosity == HealthCheckNotificationVerbosity.Detailed) { - 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); + sb.AppendFormat(", Message: '{0}'", SimpleHtmlToMarkDown(checkResult.Message)); } - 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); - } + sb.AppendLine(Environment.NewLine); } - - return sb.ToString(); } + return sb.ToString(); + } - internal Dictionary> ResultsAsDictionary => _results; - - private string SimpleHtmlToMarkDown(string html) + public Dictionary>? GetResultsForStatus(StatusResultType resultType) + { + switch (resultType) { - return html.Replace("", "**") - .Replace("", "**") - .Replace("", "*") - .Replace("", "*"); + 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); } - public Dictionary>? GetResultsForStatus(StatusResultType resultType) - { - switch (resultType) - { - 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); - } - - return null; - } + 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 49428fe8996c..7f04e5154159 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 495fc42cf17b..1c026248c80b 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 +namespace Umbraco.Cms.Core.HealthChecks; + +public class + HealthCheckCollectionBuilder : LazyCollectionBuilderBase { - public class HealthCheckCollectionBuilder : LazyCollectionBuilderBase - { - protected override HealthCheckCollectionBuilder This => this; + 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! - } + // 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 94867d8882ee..022531c1eccf 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.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; - } + Enabled = false; + return; + } - RecipientEmail = recipientEmail; + RecipientEmail = recipientEmail; - _textService = textService ?? throw new ArgumentNullException(nameof(textService)); - _hostingEnvironment = hostingEnvironment; - _emailSender = emailSender; - _markdownToHtmlConverter = markdownToHtmlConverter; - _contentSettings = contentSettings.CurrentValue ?? throw new ArgumentNullException(nameof(contentSettings)); + _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); - } + contentSettings.OnChange(x => _contentSettings = x); + } - public string? RecipientEmail { get; } + public string? RecipientEmail { get; } - public override async Task SendAsync(HealthCheckResults results) + 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(); - - var subject = _textService?.Localize("healthcheck","scheduledHealthCheckEmailSubject", new[] { host }); - + // 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 mailMessage = CreateMailMessage(subject, message); - Task? task = _emailSender?.SendAsync(mailMessage, Constants.Web.EmailTypes.HealthCheck); - if (task is not null) - { - await task; - } - } + var subject = _textService?.Localize("healthcheck", "scheduledHealthCheckEmailSubject", new[] { host }); - 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; + await task; + } + } - if (string.IsNullOrWhiteSpace(subject)) - subject = "Umbraco Health Check Status"; + private EmailMessage CreateMailMessage(string? subject, string? message) + { + var to = _contentSettings?.Notifications.Email; - 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; } + IDictionary notificationMethods = + healthCheckSettings.CurrentValue.Notification.NotificationMethods; + if (!notificationMethods.TryGetValue( + attribute.Alias, out HealthChecksNotificationMethodSettings? notificationMethod)) + { + Enabled = false; + return; + } - public bool FailureOnly { get; protected set; } + Enabled = notificationMethod.Enabled; + FailureOnly = notificationMethod.FailureOnly; + Verbosity = notificationMethod.Verbosity; + Settings = notificationMethod.Settings; + } - public HealthCheckNotificationVerbosity Verbosity { get; protected set; } + public bool FailureOnly { get; protected set; } - public IDictionary? Settings { get; } + public HealthCheckNotificationVerbosity Verbosity { get; protected set; } - protected bool ShouldSend(HealthCheckResults results) - { - return Enabled && (!FailureOnly || !results.AllChecksSuccessful); - } + public IDictionary? Settings { get; } - public abstract Task SendAsync(HealthCheckResults results); - } + 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 b06322a267e5..0516fc35448a 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 254a53c6fb91..9269f905f4d7 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 ce4df997ab1b..b95376646b25 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) + { + var length = bytes.Length; + var chars = new char[length * 2]; - /// - /// Converts a to a hexadecimal formatted padded to 2 digits. - /// - /// The bytes. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Encode(byte[] bytes) + var index = 0; + for (var i = 0; i < length; i++) { - var length = bytes.Length; - var chars = new char[length * 2]; + var byteIndex = bytes[i]; + chars[index++] = HexLutHi[byteIndex]; + chars[index++] = HexLutLo[byteIndex]; + } - 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); + } - 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; - /// - /// 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) + 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; - } + continue; + } - chars[index++] = separator; - size = 0; - count++; + if (++size < blockSize) + { + continue; } - return new string(chars, 0, chars.Length); + 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 2d1336ab905b..84b275714b03 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 c2c7cfe79256..b8960048f63d 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 f55040f96a93..493c3ab4dc1d 100644 --- a/src/Umbraco.Core/Hosting/IUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Core/Hosting/IUmbracoApplicationLifetime.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Hosting +namespace Umbraco.Cms.Core.Hosting; + +public interface IUmbracoApplicationLifetime { - public interface IUmbracoApplicationLifetime - { - /// - /// A value indicating whether the application is restarting after the current request. - /// - bool IsRestarting { get; } + /// + /// 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(); - } + /// + /// 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 15b08d1ac6fb..e821102f0997 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 3200f97d7dee..fdee8e4ec5dc 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 s_ambientContext = new AsyncLocal(); + private static readonly AsyncLocal AmbientContext = new(); - private readonly IRequestCache _requestCache; - private string? _itemKey; - protected string ItemKey => _itemKey ??= GetType().FullName!; + private readonly IRequestCache _requestCache; + private string? _itemKey; - // 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+) + protected HybridAccessorBase(IRequestCache requestCache) + => _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); - // no! - //[ThreadStatic] - //private static T _value; + protected string ItemKey => _itemKey ??= GetType().FullName!; - // yes! flows with async! - private T? NonContextValue + protected T? Value + { + get { - get => s_ambientContext.Value ?? default; - set => s_ambientContext.Value = value; - } + if (!_requestCache.IsAvailable) + { + return NonContextValue; + } - protected HybridAccessorBase(IRequestCache requestCache) - => _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + return (T?)_requestCache.Get(ItemKey); + } - protected T? Value + set { - get + if (!_requestCache.IsAvailable) { - if (!_requestCache.IsAvailable) - { - return NonContextValue; - } - return (T?) _requestCache.Get(ItemKey); + NonContextValue = value; } - - set + else if (value == null) { - if (!_requestCache.IsAvailable) - { - NonContextValue = value; - } - else if (value == null) - { - _requestCache.Remove(ItemKey); - } - else - { - _requestCache.Set(ItemKey, value); - } + _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 14fa0433ce04..d129b9a117e1 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 +namespace Umbraco.Cms.Core; + +public class HybridEventMessagesAccessor : HybridAccessorBase, IEventMessagesAccessor { - public class HybridEventMessagesAccessor : HybridAccessorBase, IEventMessagesAccessor + public HybridEventMessagesAccessor(IRequestCache requestCache) + : base(requestCache) { - public HybridEventMessagesAccessor(IRequestCache requestCache) - : base(requestCache) - { - } + } - public EventMessages? EventMessages - { - get { return Value; } - set { Value = value; } - } + public EventMessages? EventMessages + { + get => Value; + set => Value = value; } } diff --git a/src/Umbraco.Core/IBackOfficeInfo.cs b/src/Umbraco.Core/IBackOfficeInfo.cs index 66f5d97bd9e5..bc27eb7f1649 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 2061723575ec..b13000de22b1 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 d2bed317a66e..76d1767eabd2 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 CleanFolderResultStatus Status { get; private set; } - public IReadOnlyCollection? Errors { get; private set; } + public IReadOnlyCollection? Errors { get; private set; } - public static CleanFolderResult Success() - { - return new CleanFolderResult { Status = CleanFolderResultStatus.Success }; - } + public static CleanFolderResult Success() => new CleanFolderResult { Status = CleanFolderResultStatus.Success }; - public static CleanFolderResult FailedAsDoesNotExist() - { - return new CleanFolderResult { Status = CleanFolderResultStatus.FailedAsDoesNotExist }; - } + public static CleanFolderResult FailedAsDoesNotExist() => + new CleanFolderResult { Status = CleanFolderResultStatus.FailedAsDoesNotExist }; - public static CleanFolderResult FailedWithErrors(List errors) - { - return new CleanFolderResult - { - Status = CleanFolderResultStatus.FailedWithException, - Errors = errors.AsReadOnly(), - }; - } + public static CleanFolderResult FailedWithErrors(List errors) => + new CleanFolderResult { Status = CleanFolderResultStatus.FailedWithException, Errors = errors.AsReadOnly() }; - public class Error + public class Error + { + public Error(Exception exception, FileInfo erroringFile) { - public Error(Exception exception, FileInfo erroringFile) - { - Exception = exception; - ErroringFile = erroringFile; - } + Exception = exception; + ErroringFile = erroringFile; + } - public Exception Exception { get; set; } + 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 3180677acb16..73d32982aa78 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 e78118da6270..5e0c10d80d9c 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)) - modelNamespaceAlias = "ContentModels"; + var content = new StringBuilder(); - // 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 (string.IsNullOrWhiteSpace(modelNamespaceAlias)) + { + modelNamespaceAlias = "ContentModels"; + } - // if required, add - // @using ContentModels = ModelNamespace; - if (modelClassName.IsNullOrWhiteSpace() == false && modelNamespace.IsNullOrWhiteSpace() == false) + // 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("@using "); content.Append(modelNamespaceAlias); - content.Append(" = "); - content.Append(modelNamespace); - content.Append(";\r\n"); + content.Append("."); } - // 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(); + 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 16ac1b00415e..44bc1ac2ad5b 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); - } + fileStream.Seek(0, SeekOrigin.Begin); + } - using HashAlgorithm alg = SHA1.Create(); + 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(); + // 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")); } - /// - /// 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; + return stringBuilder.ToString(); + } - while (retries > 0) + /// + /// 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); - } + 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); - } - } + throw new ArgumentException("Retries must be greater than zero"); + } - public static string GetExtension(this IFileSystem fs, string path) + public static void CopyFile(this IFileSystem fs, string path, string newPath) + { + using (Stream stream = fs.OpenFile(path)) { - return Path.GetExtension(fs.GetFullPath(path)); + fs.AddFile(newPath, stream); } + } - public static string GetFileName(this IFileSystem fs, string path) - { - return Path.GetFileName(fs.GetFullPath(path)); - } + public static string GetExtension(this IFileSystem fs, string path) => Path.GetExtension(fs.GetFullPath(path)); - // TODO: Currently this is the only way to do this - public static void CreateFolder(this IFileSystem fs, string folderPath) + 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()) { - 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); + fs.AddFile(tempFile, s); } - /// - /// 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) + 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 { - fileProvider = fileSystem switch - { - IFileProviderFactory fileProviderFactory => fileProviderFactory.Create(), - _ => null - }; + IFileProviderFactory fileProviderFactory => fileProviderFactory.Create(), + _ => null, + }; - return fileProvider != null; - } + return fileProvider != null; } } diff --git a/src/Umbraco.Core/IO/FileSystems.cs b/src/Umbraco.Core/IO/FileSystems.cs index 5a4c92d50990..2a5fa685df02 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 @@ public sealed class FileSystems 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 @@ public void SetStylesheetFilesystem(IFileSystem fileSystem) // 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 @@ public void SetStylesheetFilesystem(IFileSystem fileSystem) "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 @@ public void SetStylesheetFilesystem(IFileSystem fileSystem) // 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 @@ public void SetStylesheetFilesystem(IFileSystem fileSystem) 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 a2937f3f8e75..3ca1fadbff10 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 981d5558fc2c..0e6cb0f0a8f2 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 54503b167b99..da9dd0b9bba4 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. - /// - public interface IFileSystem - { - /// - /// 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. - /// - /// The name of the directory to remove. - void DeleteDirectory(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); - - /// - /// 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. - void AddFile(string path, Stream stream); - - /// - /// 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. - /// - /// The path to the files. - /// - /// The representing the matched files. - /// - IEnumerable GetFiles(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); - - /// - /// Gets a representing the file at the given path. - /// - /// The path to the file. - /// - /// . - /// - Stream OpenFile(string path); - - /// - /// Deletes the specified file. - /// - /// The name of the file to remove. - void DeleteFile(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); - - /// - /// Returns the application relative path to the file. - /// - /// The full path or URL. - /// - /// The representing the relative path. - /// - string GetRelativePath(string fullPathOrUrl); - - /// - /// 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 URL to the file. - /// - /// The path to return the URL for. - /// - /// representing the relative URL. - /// - string GetUrl(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 created date/time of the file, expressed as a UTC value. - /// - /// The path to the file. - /// - /// . - /// - DateTimeOffset GetCreated(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 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; } - - /// - /// 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); - } + /// 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 all directories matching the given path. + /// + /// The path to the directories. + /// + /// The representing the matched directories. + /// + IEnumerable GetDirectories(string path); + + /// + /// Deletes the specified directory. + /// + /// The name of the directory to remove. + void DeleteDirectory(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); + + /// + /// 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. + void AddFile(string path, Stream stream); + + /// + /// 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. + /// + /// The path to the files. + /// + /// The representing the matched files. + /// + IEnumerable GetFiles(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); + + /// + /// Gets a representing the file at the given path. + /// + /// The path to the file. + /// + /// . + /// + Stream OpenFile(string path); + + /// + /// Deletes the specified file. + /// + /// The name of the file to remove. + void DeleteFile(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); + + /// + /// Returns the application relative path to the file. + /// + /// The full path or URL. + /// + /// The representing the relative path. + /// + string GetRelativePath(string fullPathOrUrl); + + /// + /// 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 URL to the file. + /// + /// The path to return the URL for. + /// + /// representing the relative URL. + /// + string GetUrl(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 created date/time of the file, expressed as a UTC value. + /// + /// The path to the file. + /// + /// . + /// + DateTimeOffset GetCreated(string path); + + /// + /// 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); + + // 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 5a814ab386db..53376dd48b2f 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 da9a06d1b115..70ed6c7a3bd0 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 d0f190868b6d..cffd2780daf7 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); + retval = _hostingEnvironment.ApplicationVirtualPath + "/" + + virtualPath.TrimStart(Constants.CharArrays.ForwardSlash); + } - if (virtualPath.StartsWith("/") && !PathStartsWith(virtualPath, _hostingEnvironment.ApplicationVirtualPath)) - retval = _hostingEnvironment.ApplicationVirtualPath + "/" + virtualPath.TrimStart(Constants.CharArrays.ForwardSlash); + return retval; + } - 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; } - // 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 _hostingEnvironment.ToAbsolute(virtualPath); + } + public string MapPath(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); } - public string MapPath(string 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)) { - if (path == null) throw new ArgumentNullException(nameof(path)); + return 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 (_hostingEnvironment.IsHosted) + if (result != null) { - 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 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; + 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; - } + 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); - - - /// - /// 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) + /// + /// 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)) { - return VerifyEditPath(filePath, new[] { validDir }); + // TODO this is going to fail.. Scripts Stylesheets need to use WebRoot, PartialViews need to use ContentRoot + filePath = _hostingEnvironment.MapPathWebRoot(filePath); } - /// - /// 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) + // 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) { - // 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 validDir = dir; + if (!PathStartsWith(validDir, mappedRoot)) + { + validDir = _hostingEnvironment.MapPathWebRoot(validDir); + } - var mappedRoot = MapPath(_hostingEnvironment.ApplicationVirtualPath); - if (!PathStartsWith(filePath, mappedRoot)) + if (PathStartsWith(filePath, validDir)) { - // TODO this is going to fail.. Scripts Stylesheets need to use WebRoot, PartialViews need to use ContentRoot - filePath = _hostingEnvironment.MapPathWebRoot(filePath); + return true; } + } - // yes we can (see above) - //// don't trust what we get, it may contain relative segments - //filePath = Path.GetFullPath(filePath); + return false; + } - foreach (var dir in validDirs) - { - var validDir = dir; - if (!PathStartsWith(validDir, mappedRoot)) - validDir = _hostingEnvironment.MapPathWebRoot(validDir); + /// + /// 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)); + } - if (PathStartsWith(filePath, validDir)) - return true; - } + public abstract bool PathStartsWith(string path, string root, params char[] separators); - return false; + public void EnsurePathExists(string path) + { + var absolutePath = MapPath(path); + if (Directory.Exists(absolutePath) == false) + { + Directory.CreateDirectory(absolutePath); } + } - /// - /// 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) + /// + /// Get properly formatted relative path from an existing absolute or relative path + /// + /// + /// + public string GetRelativePath(string path) + { + if (path.IsFullPath()) { - var ext = Path.GetExtension(filePath); - return ext != null && validFileExtensions.Contains(ext.TrimStart(Constants.CharArrays.Period)); + var rootDirectory = MapPath("~"); + var relativePath = PathStartsWith(path, rootDirectory) ? path[rootDirectory.Length..] : path; + path = relativePath; } - public abstract bool PathStartsWith(string path, string root, params char[] separators); + return PathUtility.EnsurePathIsApplicationRootPrefixed(path); + } - public void EnsurePathExists(string path) + /// + /// Retrieves array of temporary folders from the hosting environment. + /// + /// Array of instances. + public DirectoryInfo[] GetTempFolders() + { + var tempFolderPaths = new[] { - var absolutePath = MapPath(path); - if (Directory.Exists(absolutePath) == false) - Directory.CreateDirectory(absolutePath); - } + _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads), + }; - /// - /// Get properly formatted relative path from an existing absolute or relative path - /// - /// - /// - public string GetRelativePath(string path) + foreach (var tempFolderPath in tempFolderPaths) { - if (path.IsFullPath()) - { - var rootDirectory = MapPath("~"); - var relativePath = PathStartsWith(path, rootDirectory) ? path.Substring(rootDirectory.Length) : path; - path = relativePath; - } - - return PathUtility.EnsurePathIsApplicationRootPrefixed(path); + // Ensure it exists + Directory.CreateDirectory(tempFolderPath); } - /// - /// Retrieves array of temporary folders from the hosting environment. - /// - /// Array of instances. - public DirectoryInfo[] GetTempFolders() - { - var tempFolderPaths = new[] - { - _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads) - }; + return tempFolderPaths.Select(x => new DirectoryInfo(x)).ToArray(); + } - foreach (var tempFolderPath in tempFolderPaths) - { - // Ensure it exists - Directory.CreateDirectory(tempFolderPath); - } + /// + /// 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. - return tempFolderPaths.Select(x => new DirectoryInfo(x)).ToArray(); + if (!folder.Exists) + { + return CleanFolderResult.FailedAsDoesNotExist(); } - /// - /// 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) + FileInfo[] files = folder.GetFiles("*.*", SearchOption.AllDirectories); + var errors = new List(); + foreach (FileInfo file in files) { - 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) { - if (DateTime.UtcNow - file.LastWriteTimeUtc > age) + try + { + file.IsReadOnly = false; + file.Delete(); + } + catch (Exception ex) { - try - { - file.IsReadOnly = false; - file.Delete(); - } - catch (Exception ex) - { - errors.Add(new CleanFolderResult.Error(ex, file)); - } + errors.Add(new CleanFolderResult.Error(ex, file)); } } - - return errors.Any() - ? CleanFolderResult.FailedWithErrors(errors) - : CleanFolderResult.Success(); } + + 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 1625c239ff00..7ae90e7f8ebd 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); + return path.StartsWith("~/") ? ioHelper.ResolveUrl(path) : path; + } - if (Directory.Exists(dirPath) == false) - Directory.CreateDirectory(dirPath); + /// + /// 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); - 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 + if (Directory.Exists(dirPath) == false) { - return false; + Directory.CreateDirectory(dirPath); } - } - public static string CreateRandomFileName(this IIOHelper ioHelper) + 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 "umbraco-test." + Guid.NewGuid().ToString("N").Substring(0, 8); + 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 116a7200b3a5..7d936895a1f3 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 override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path); + + public override bool PathStartsWith(string path, string root, params char[] separators) { - public IOHelperLinux(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment) + // 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); + if (!path.StartsWith(root, StringComparison.Ordinal)) + { + return false; + } - public override bool PathStartsWith(string path, string root, params char[] separators) + if (path.Length == root.Length) { - // 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 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 53b9cb4dc027..8b8ed2093959 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 override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path); + + public override bool PathStartsWith(string path, string root, params char[] separators) { - public IOHelperOSX(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment) + // 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); + if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) + { + return false; + } - public override bool PathStartsWith(string path, string root, params char[] separators) + if (path.Length == root.Length) { - // 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 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 cb60f164dcf0..9dfec76f3603 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 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; + } + + 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')); + } + + public override bool PathStartsWith(string path, string root, params char[] separators) { - public IOHelperWindows(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment) + // 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) + if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) { - // 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')); + return false; } - public override bool PathStartsWith(string path, string root, params char[] separators) + if (path.Length == root.Length) { - // 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 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 ae6f8698a48b..f84a1ba25655 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 d5c421721e81..c222c017448a 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.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; - } + _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(); + [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 }; + // kinda try to keep things under control + var options = new ParallelOptions { MaxDegreeOfParallelism = 20 }; - Parallel.ForEach(files, options, file => + Parallel.ForEach(files, options, file => + { + try { - try + if (file.IsNullOrWhiteSpace()) { - 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); - } + return; } - catch (Exception e) + + if (FileSystem.FileExists(file) == false) { - _logger.LogError(e, "Failed to delete media file '{File}'.", file); + return; } - }); - } - #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) + FileSystem.DeleteFile(file); + + var directory = _mediaPathScheme.GetDeleteDirectory(this, file); + if (!directory.IsNullOrWhiteSpace()) + { + FileSystem.DeleteDirectory(directory!, true); + } + } + catch (Exception e) { - throw new ArgumentException("Cannot become a safe filename.", nameof(filename)); + _logger.LogError(e, "Failed to delete media file '{File}'.", file); } + }); + } - filename = _shortStringHelper.CleanStringForSafeFileName(filename.ToLowerInvariant()); - - return _mediaPathScheme.GetFilePath(this, cuid, puid, filename); + #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)); } - #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(); - } + filename = _shortStringHelper.CleanStringForSafeFileName(filename.ToLowerInvariant()); - if (!content.TryGetMediaPath(propertyTypeAlias, _mediaUrlGenerators!, out mediaFilePath, culture, segment)) - { - return Stream.Null; - } + return _mediaPathScheme.GetFilePath(this, cuid, puid, filename); + } - return FileSystem.OpenFile(mediaFilePath!); + #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(); } - /// - /// 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.TryGetMediaPath(propertyTypeAlias, _mediaUrlGenerators!, out mediaFilePath, culture, segment)) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } + return Stream.Null; + } - if (propertyType == null) - { - throw new ArgumentNullException(nameof(propertyType)); - } + return FileSystem.OpenFile(mediaFilePath!); + } - if (filename == null) - { - throw new ArgumentNullException(nameof(filename)); - } + /// + /// 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 (string.IsNullOrWhiteSpace(filename)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(filename)); - } + if (propertyType == null) + { + throw new ArgumentNullException(nameof(propertyType)); + } - if (filestream == null) - { - throw new ArgumentNullException(nameof(filestream)); - } + if (filename == null) + { + throw new ArgumentNullException(nameof(filename)); + } - // clear the old file, if any - if (string.IsNullOrWhiteSpace(oldpath) == false) - { - FileSystem.DeleteFile(oldpath!); - } + if (string.IsNullOrWhiteSpace(filename)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(filename)); + } - // get the filepath, store the data - var filepath = GetMediaPath(filename, content.Key, propertyType.Key); - FileSystem.AddFile(filepath, filestream); - return filepath; + if (filestream == null) + { + throw new ArgumentNullException(nameof(filestream)); } - /// - /// 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) + // clear the old file, if any + if (string.IsNullOrWhiteSpace(oldpath) == false) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } + FileSystem.DeleteFile(oldpath); + } - if (propertyType == null) - { - throw new ArgumentNullException(nameof(propertyType)); - } + // get the filepath, store the data + var filepath = GetMediaPath(filename, content.Key, propertyType.Key); + FileSystem.AddFile(filepath, filestream); + return filepath; + } - if (sourcepath == null) - { - throw new ArgumentNullException(nameof(sourcepath)); - } + /// + /// 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 (string.IsNullOrWhiteSpace(sourcepath)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(sourcepath)); - } + if (propertyType == null) + { + throw new ArgumentNullException(nameof(propertyType)); + } - // ensure we have a file to copy - if (FileSystem.FileExists(sourcepath) == false) - { - return null; - } + if (sourcepath == null) + { + throw new ArgumentNullException(nameof(sourcepath)); + } - // get the filepath - var filename = Path.GetFileName(sourcepath); - var filepath = GetMediaPath(filename, content.Key, propertyType.Key); - FileSystem.CopyFile(sourcepath, filepath); - return filepath; + if (string.IsNullOrWhiteSpace(sourcepath)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(sourcepath)); } - #endregion + // 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 5adc81276b99..b73d29df6036 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 1ee821e3ed5a..a533a62c921f 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 a3fe36bde914..7b7061506d7e 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) - { - var combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid); - var directory = GuidUtils.ToBase32String(combinedGuid, DirectoryLength); + private const int DirectoryLength = 8; - return Path.Combine(directory, filename).Replace('\\', '/'); - } + /// + public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) + { + Guid combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid); + var directory = GuidUtils.ToBase32String(combinedGuid, DirectoryLength); - /// - /// - /// 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 30d1893792e5..ede481b83344 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 @@ public PhysicalFileSystem(IIOHelper ioHelper, IHostingEnvironment hostingEnviron _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 @@ public IEnumerable GetDirectories(string path) try { if (Directory.Exists(fullPath)) + { return Directory.EnumerateDirectories(fullPath).Select(GetRelativePath); + } } catch (UnauthorizedAccessException ex) { @@ -103,7 +119,9 @@ public void DeleteDirectory(string path, bool recursive) { var fullPath = GetFullPath(path); if (Directory.Exists(fullPath) == false) + { return; + } try { @@ -154,7 +172,11 @@ public void AddFile(string path, Stream stream, bool overrideExisting) } 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 @@ public IEnumerable GetFiles(string path, string filter) try { if (Directory.Exists(fullPath)) + { return Directory.EnumerateFiles(fullPath, filter).Select(GetRelativePath); + } } catch (UnauthorizedAccessException ex) { @@ -224,7 +248,9 @@ public void DeleteFile(string path) { var fullPath = GetFullPath(path); if (File.Exists(fullPath) == false) + { return; + } try { @@ -265,12 +291,16 @@ public string GetRelativePath(string fullPathOrUrl) // 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 @@ public string GetFullPath(string path) // 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 @@ public string GetFullPath(string path) // 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 @@ public void AddFile(string path, string physicalPath, bool overrideIfExists = tr 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 @@ protected void WithRetry(Action action) // 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 bd3f9d770d3e..95517f805445 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) + { + Inner = fs; + _sfs = sfs; + } + + public IFileSystem Inner { get; } + + public bool CanAddPhysical => true; + + private Dictionary Nodes => _nodes ??= new Dictionary(); + + public IEnumerable GetDirectories(string path) { - private readonly IFileSystem _fs; - private readonly IFileSystem _sfs; + 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 ShadowFileSystem(IFileSystem fs, IFileSystem sfs) + public void DeleteDirectory(string path, bool recursive) + { + if (DirectoryExists(path) == false) { - _fs = fs; - _sfs = sfs; + return; } - public IFileSystem Inner => _fs; + var normPath = NormPath(path); + if (recursive) + { + Nodes[normPath] = new ShadowNode(true, true); + var remove = Nodes.Where(x => IsDescendant(normPath, x.Key)).ToList(); + foreach (KeyValuePair kvp in remove) + { + Nodes.Remove(kvp.Key); + } - public void Complete() + Delete(path, true); + } + else { - if (_nodes == null) return; - var exceptions = new List(); - foreach (var kvp in _nodes) + // actual content + if (Nodes.Any(x => IsChild(normPath, x.Key) && x.Value.IsExist) // shadow content + || Inner.GetDirectories(path).Any() || Inner.GetFiles(path).Any()) + { + 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) { - if (kvp.Value.IsExist) + 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) { - 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)); - } - } + throw new InvalidOperationException("Invalid path."); } - else + + if (sd.IsDelete) { - 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[dirPath] = new ShadowNode(false, true); } } - _nodes.Clear(); + else + { + if (Inner.DirectoryExists(dirPath)) + { + continue; + } + + if (Inner.FileExists(dirPath)) + { + throw new InvalidOperationException("Invalid path."); + } - if (exceptions.Count == 0) return; - throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); + Nodes[dirPath] = new ShadowNode(false, true); + } } - private Dictionary? _nodes; + _sfs.AddFile(path, stream, overrideIfExists); + Nodes[normPath] = new ShadowNode(false, false); + } + + public IEnumerable GetFiles(string path) => GetFiles(path, null); - private Dictionary Nodes => _nodes ?? (_nodes = new Dictionary()); + 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(); + } - private class ShadowNode + public Stream OpenFile(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf)) { - public ShadowNode(bool isDelete, bool isdir) - { - IsDelete = isDelete; - IsDir = isdir; - } + return sf.IsDir || sf.IsDelete ? Stream.Null : _sfs.OpenFile(path); + } - public bool IsDelete { get; } - public bool IsDir { get; } + return Inner.OpenFile(path); + } - public bool IsExist => IsDelete == false; - public bool IsFile => IsDir == false; + public void DeleteFile(string path) + { + if (FileExists(path) == false) + { + return; } - private static string NormPath(string path) + Nodes[NormPath(path)] = new ShadowNode(true, false); + } + + public bool FileExists(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf)) { - return path.ToLowerInvariant().Replace("\\", "/"); + return sf.IsFile && sf.IsExist; } - // values can be "" (root), "foo", "foo/bar"... - private static bool IsChild(string path, string input) + 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)) { - 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; + return sf.IsDir || sf.IsDelete ? string.Empty : _sfs.GetFullPath(path); } - private static bool IsDescendant(string path, string input) + 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) { - if (input.StartsWith(path) == false || input.Length < path.Length + 2) - return false; - return path.Length == 0 || input[path.Length] == '/'; + return Inner.GetLastModified(path); } - public IEnumerable GetDirectories(string path) + if (sf.IsDelete) { - 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(); + throw new InvalidOperationException("Invalid path."); } - public void DeleteDirectory(string path) + return _sfs.GetLastModified(path); + } + + public DateTimeOffset GetCreated(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf) == false) { - DeleteDirectory(path, false); + return Inner.GetCreated(path); } - public void DeleteDirectory(string path, bool recursive) + if (sf.IsDelete) { - if (DirectoryExists(path) == false) return; - var normPath = NormPath(path); - if (recursive) - { - 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); - } - 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); - } + throw new InvalidOperationException("Invalid path."); } - private void Delete(string path, bool recurse) + return _sfs.GetCreated(path); + } + + public long GetSize(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf) == false) { - 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); - } + return Inner.GetSize(path); } - public bool DirectoryExists(string path) + if (sf.IsDelete || sf.IsDir) { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf)) - return sf.IsDir && sf.IsExist; - return _fs.DirectoryExists(path); + throw new InvalidOperationException("Invalid path."); } - public void AddFile(string path, Stream stream) + 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)) { - AddFile(path, stream, true); + throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); } - public void AddFile(string path, Stream stream, bool overrideIfExists) + var parts = normPath.Split(Constants.CharArrays.ForwardSlash); + for (var i = 0; i < parts.Length - 1; i++) { - 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)); + if (Nodes.TryGetValue(dirPath, out ShadowNode? sd)) { - var dirPath = string.Join("/", parts.Take(i + 1)); - ShadowNode? sd; - if (Nodes.TryGetValue(dirPath, out sd)) + if (sd.IsFile) { - if (sd.IsFile) throw new InvalidOperationException("Invalid path."); - if (sd.IsDelete) Nodes[dirPath] = new ShadowNode(false, true); + throw new InvalidOperationException("Invalid path."); } - else + + if (sd.IsDelete) { - if (_fs.DirectoryExists(dirPath)) continue; - if (_fs.FileExists(dirPath)) throw new InvalidOperationException("Invalid path."); Nodes[dirPath] = new ShadowNode(false, true); } } + else + { + if (Inner.DirectoryExists(dirPath)) + { + continue; + } - _sfs.AddFile(path, stream, overrideIfExists); - Nodes[normPath] = new ShadowNode(false, false); - } + if (Inner.FileExists(dirPath)) + { + throw new InvalidOperationException("Invalid path."); + } - public IEnumerable GetFiles(string path) - { - return GetFiles(path, null); + Nodes[dirPath] = new ShadowNode(false, true); + } } - public IEnumerable GetFiles(string path, string? filter) + _sfs.AddFile(path, physicalPath, overrideIfExists, copy); + Nodes[normPath] = new ShadowNode(false, false); + } + + public void Complete() + { + if (_nodes == null) { - 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(); + return; } - public Stream OpenFile(string path) + var exceptions = new List(); + foreach (KeyValuePair kvp in _nodes) { - if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf)) + if (kvp.Value.IsExist) { - return sf.IsDir || sf.IsDelete ? Stream.Null : _sfs.OpenFile(path); + 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)); + } } - - return _fs.OpenFile(path); } - public void DeleteFile(string path) + _nodes.Clear(); + + if (exceptions.Count == 0) { - if (FileExists(path) == false) return; - Nodes[NormPath(path)] = new ShadowNode(true, false); + return; } - public bool FileExists(string path) + throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); + } + + 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) { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf)) - return sf.IsFile && sf.IsExist; - return _fs.FileExists(path); + return false; } - public string GetRelativePath(string fullPathOrUrl) + if (path.Length > 0 && input[path.Length] != '/') { - return _fs.GetRelativePath(fullPathOrUrl); + return false; } - public string GetFullPath(string path) + 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) { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf)) - return sf.IsDir || sf.IsDelete ? string.Empty : _sfs.GetFullPath(path); - return _fs.GetFullPath(path); + return false; } - public string GetUrl(string? path) + return path.Length == 0 || input[path.Length] == '/'; + } + + private void Delete(string path, bool recurse) + { + foreach (var file in Inner.GetFiles(path)) { - return _fs.GetUrl(path); + Nodes[NormPath(file)] = new ShadowNode(true, false); } - public DateTimeOffset GetLastModified(string path) + foreach (var dir in Inner.GetDirectories(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); + Nodes[NormPath(dir)] = new ShadowNode(true, true); + if (recurse) + { + Delete(dir, true); + } } + } - public DateTimeOffset GetCreated(string path) + // 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) { - 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); + _pattern = pattern; + _caseInsensitive = caseInsensitive; } - public long GetSize(string path) + public bool IsMatch(string input) { - 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); + EnsureRegex(_pattern); + return _regex?.IsMatch(input) ?? false; } - public bool CanAddPhysical { get { return true; } } - - public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) + private void EnsureRegex(string pattern) { - 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++) + if (_regex != null) { - var dirPath = string.Join("/", parts.Take(i + 1)); - ShadowNode? sd; - if (Nodes.TryGetValue(dirPath, out sd)) - { - if (sd.IsFile) throw new InvalidOperationException("Invalid path."); - if (sd.IsDelete) Nodes[dirPath] = new ShadowNode(false, true); - } - else - { - if (_fs.DirectoryExists(dirPath)) continue; - if (_fs.FileExists(dirPath)) throw new InvalidOperationException("Invalid path."); - Nodes[dirPath] = new ShadowNode(false, true); - } + return; } - _sfs.AddFile(path, physicalPath, overrideIfExists, copy); - Nodes[normPath] = new ShadowNode(false, false); - } + RegexOptions options = RegexOptions.None; - // copied from System.Web.Util.Wildcard internal - internal class WildcardExpression - { - private readonly string _pattern; - private readonly bool _caseInsensitive; - private Regex? _regex; - - 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("(?=[\\\\:])"); - - public WildcardExpression(string pattern, bool caseInsensitive = true) + // match right-to-left (for speed) if the pattern starts with a * + if (pattern.Length > 0 && pattern[0] == '*') { - _pattern = pattern; - _caseInsensitive = caseInsensitive; + options = RegexOptions.RightToLeft | RegexOptions.Singleline; } - - private void EnsureRegex(string pattern) + else { - 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; + options = RegexOptions.Singleline; + } - // case insensitivity + // case insensitivity + if (_caseInsensitive) + { + options |= RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; + } - if (_caseInsensitive) - options |= RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; + // Remove regex metacharacters + pattern = MetaRegex.Replace(pattern, "\\$0"); - // Remove regex metacharacters + // Replace wildcard metacharacters with regex codes + pattern = QuestRegex.Replace(pattern, "."); + pattern = StarRegex.Replace(pattern, ".*"); + pattern = CommaRegex.Replace(pattern, "\\z|\\A"); - pattern = metaRegex.Replace(pattern, "\\$0"); + // anchor the pattern at beginning and end, and return the regex + _regex = new Regex("\\A" + pattern + "\\z", options); + } + } - // Replace wildcard metacharacters with regex codes + private class ShadowNode + { + public ShadowNode(bool isDelete, bool isdir) + { + IsDelete = isDelete; + IsDir = isdir; + } - pattern = questRegex.Replace(pattern, "."); - pattern = starRegex.Replace(pattern, ".*"); - pattern = commaRegex.Replace(pattern, "\\z|\\A"); + public bool IsDelete { get; } - // anchor the pattern at beginning and end, and return the regex + public bool IsDir { get; } - _regex = new Regex("\\A" + pattern + "\\z", options); - } + public bool IsExist => IsDelete == false; - public bool IsMatch(string input) - { - EnsureRegex(_pattern); - return _regex?.IsMatch(input) ?? false; - } - } + public bool IsFile => IsDir == false; } } diff --git a/src/Umbraco.Core/IO/ShadowFileSystems.cs b/src/Umbraco.Core/IO/ShadowFileSystems.cs index 413cc73d8a38..3d69875dc4d6 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; - - // invoked by the filesystems when shadowing - public ShadowFileSystems(FileSystems fileSystems, string id) - { - _fileSystems = fileSystems; - Id = id; + _fileSystems = fileSystems; + Id = id; - _fileSystems.BeginShadow(id); - } + _fileSystems.BeginShadow(id); + } - // for tests - public string Id { get; } + // 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, if completed + public void Complete() => _completed = true; - // invoked by the scope when exiting - public void Dispose() - { - _fileSystems.EndShadow(Id, _completed); - } - } + // 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 67808e1fdb57..2f2e0a991cb0 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"; - - 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 ShadowWrapper(IFileSystem innerFileSystem, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILoggerFactory loggerFactory, string shadowPath, Func? isScoped = null) - { - _innerFileSystem = innerFileSystem; - _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); - _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); - _loggerFactory = loggerFactory; - _shadowPath = shadowPath; - _isScoped = isScoped; - } + 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 + public IFileSystem InnerFileSystem { get; } - // 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) + public bool CanAddPhysical => FileSystem.CanAddPhysical; - for (var i = 0; i < retries; i++) + private IFileSystem FileSystem + { + get + { + 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; } + } - 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); - } + /// + public IFileProvider? Create() => + InnerFileSystem.TryCreateFileProvider(out IFileProvider? fileProvider) ? fileProvider : null; - internal void UnShadow(bool complete) - { - var shadowFileSystem = _shadowFileSystem; - var dir = _shadowDir; - _shadowFileSystem = null; - _shadowDir = null; + public IEnumerable GetDirectories(string path) => FileSystem.GetDirectories(path); - 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); + public void DeleteDirectory(string path) => FileSystem.DeleteDirectory(path); - // 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) - Directory.Delete(dir, true); - else - break; - pos = dir.LastIndexOf(Path.DirectorySeparatorChar); - } - } - catch - { - // ugly, isn't it? but if we cannot cleanup, bah, just leave it there - } - } - } + public void DeleteDirectory(string path, bool recursive) => FileSystem.DeleteDirectory(path, recursive); - public IFileSystem InnerFileSystem => _innerFileSystem; + public bool DirectoryExists(string path) => FileSystem.DirectoryExists(path); - private IFileSystem FileSystem - { - get - { - if (_isScoped is not null && _shadowFileSystem is not null) - { - var isScoped = _isScoped!(); + public void AddFile(string path, Stream stream) => FileSystem.AddFile(path, stream); - // 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."); + public void AddFile(string path, Stream stream, bool overrideExisting) => + FileSystem.AddFile(path, stream, overrideExisting); - return isScoped.HasValue && isScoped.Value - ? _shadowFileSystem - : _innerFileSystem; - } + public IEnumerable GetFiles(string path) => FileSystem.GetFiles(path); - return _innerFileSystem; - } - } + public IEnumerable GetFiles(string path, string filter) => FileSystem.GetFiles(path, filter); - public IEnumerable GetDirectories(string path) - { - return FileSystem.GetDirectories(path); - } + public Stream OpenFile(string path) => FileSystem.OpenFile(path); - public void DeleteDirectory(string path) - { - FileSystem.DeleteDirectory(path); - } + public void DeleteFile(string path) => FileSystem.DeleteFile(path); - public void DeleteDirectory(string path, bool recursive) - { - FileSystem.DeleteDirectory(path, recursive); - } + public bool FileExists(string path) => FileSystem.FileExists(path); - public bool DirectoryExists(string path) - { - return FileSystem.DirectoryExists(path); - } + public string GetRelativePath(string fullPathOrUrl) => FileSystem.GetRelativePath(fullPathOrUrl); - public void AddFile(string path, Stream stream) - { - FileSystem.AddFile(path, stream); - } + public string GetFullPath(string path) => FileSystem.GetFullPath(path); - public void AddFile(string path, Stream stream, bool overrideExisting) - { - FileSystem.AddFile(path, stream, overrideExisting); - } + public string GetUrl(string? path) => FileSystem.GetUrl(path); - public IEnumerable GetFiles(string path) - { - return FileSystem.GetFiles(path); - } + public DateTimeOffset GetLastModified(string path) => FileSystem.GetLastModified(path); - public IEnumerable GetFiles(string path, string filter) - { - return FileSystem.GetFiles(path, filter); - } + public DateTimeOffset GetCreated(string path) => FileSystem.GetCreated(path); - public Stream OpenFile(string path) - { - return FileSystem.OpenFile(path); - } + public long GetSize(string path) => FileSystem.GetSize(path); - public void DeleteFile(string path) - { - FileSystem.DeleteFile(path); - } + public static string CreateShadowId(IHostingEnvironment hostingEnvironment) + { + const int retries = 50; // avoid infinite loop + const int idLength = 8; // 6 chars - public bool FileExists(string path) + // 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++) { - return FileSystem.FileExists(path); - } + var id = GuidUtils.ToBase32String(Guid.NewGuid(), idLength); - public string GetRelativePath(string fullPathOrUrl) - { - return FileSystem.GetRelativePath(fullPathOrUrl); - } + var virt = ShadowFsPath + "/" + id; + var shadowDir = hostingEnvironment.MapPathContentRoot(virt); + if (Directory.Exists(shadowDir)) + { + continue; + } - public string GetFullPath(string path) - { - return FileSystem.GetFullPath(path); + Directory.CreateDirectory(shadowDir); + return id; } - public string GetUrl(string? path) - { - return FileSystem.GetUrl(path); - } + throw new Exception($"Could not get a shadow identifier (tried {retries} times)"); + } - public DateTimeOffset GetLastModified(string path) - { - return FileSystem.GetLastModified(path); - } + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) => + FileSystem.AddFile(path, physicalPath, overrideIfExists, copy); - public DateTimeOffset GetCreated(string path) - { - return FileSystem.GetCreated(path); - } + 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); + } - public long GetSize(string path) + internal void UnShadow(bool complete) + { + ShadowFileSystem? shadowFileSystem = _shadowFileSystem; + var dir = _shadowDir; + _shadowFileSystem = null; + _shadowDir = null; + + try { - return FileSystem.GetSize(path); + // 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); - public bool CanAddPhysical => FileSystem.CanAddPhysical; + // 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) + { + Directory.Delete(dir, true); + } + else + { + break; + } - public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) - { - FileSystem.AddFile(path, physicalPath, overrideIfExists, copy); + pos = dir.LastIndexOf(Path.DirectorySeparatorChar); + } + } + catch + { + // ugly, isn't it? but if we cannot cleanup, bah, just leave it there + } } - - /// - 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 9bf87c3407ff..e2502e466961 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) - { - _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 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 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); - public string GetFileContents(ITemplate t) + if (_viewFileSystem.FileExists(path)) { - 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) - { - viewContent = SaveTemplateToFile(t); - } - else - { - using (var tr = new StreamReader(_viewFileSystem.OpenFile(path))) - { - viewContent = tr.ReadToEnd(); - tr.Close(); - } - } + public string CreateView(ITemplate t, bool overWrite = false) + { + string viewContent; + var path = ViewPath(t.Alias); - return viewContent; + if (_viewFileSystem.FileExists(path) == false || overWrite) + { + viewContent = SaveTemplateToFile(t); } - - [Obsolete("Inject IDefaultViewContentProvider instead")] - public static string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, - string? modelNamespace = null, string? modelNamespaceAlias = null) + else { - var viewContentProvider = StaticServiceProvider.Instance.GetRequiredService(); - return viewContentProvider.GetDefaultFileContent(layoutPageAlias, modelClassName, modelNamespace, - modelNamespaceAlias); + using (var tr = new StreamReader(_viewFileSystem.OpenFile(path))) + { + viewContent = tr.ReadToEnd(); + tr.Close(); + } } - private string SaveTemplateToFile(ITemplate template) - { - var design = template.Content.IsNullOrWhiteSpace() ? EnsureInheritedLayout(template) : template.Content!; - var path = ViewPath(template.Alias); + return viewContent; + } - var data = Encoding.UTF8.GetBytes(design); - var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); + public string? UpdateViewFile(ITemplate t, string? currentAlias = null) + { + var path = ViewPath(t.Alias); - using (var ms = new MemoryStream(withBom)) + if (string.IsNullOrEmpty(currentAlias) == false && currentAlias != t.Alias) + { + // then kill the old file.. + var oldFile = ViewPath(currentAlias); + if (_viewFileSystem.FileExists(oldFile)) { - _viewFileSystem.AddFile(path, ms, true); + _viewFileSystem.DeleteFile(oldFile); } - - return design; } - public string? UpdateViewFile(ITemplate t, string? currentAlias = 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 path = ViewPath(t.Alias); + _viewFileSystem.AddFile(path, ms, true); + } - if (string.IsNullOrEmpty(currentAlias) == false && currentAlias != t.Alias) - { - //then kill the old file.. - var oldFile = ViewPath(currentAlias); - if (_viewFileSystem.FileExists(oldFile)) - _viewFileSystem.DeleteFile(oldFile); - } + return t.Content; + } - var data = Encoding.UTF8.GetBytes(t.Content ?? string.Empty); - var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); + public string ViewPath(string alias) => _viewFileSystem.GetRelativePath(alias.Replace(" ", string.Empty) + ".cshtml"); - using (var ms = new MemoryStream(withBom)) - { - _viewFileSystem.AddFile(path, ms, true); - } - return t.Content; - } + 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(); - public string ViewPath(string alias) + using (var ms = new MemoryStream(withBom)) { - return _viewFileSystem.GetRelativePath(alias.Replace(" ", "") + ".cshtml"); + _viewFileSystem.AddFile(path, ms, true); } - private string EnsureInheritedLayout(ITemplate template) - { - var design = template.Content; + return design; + } - if (string.IsNullOrEmpty(design)) - design = _defaultViewContentProvider.GetDefaultFileContent(template.MasterTemplateAlias); + private string EnsureInheritedLayout(ITemplate template) + { + var design = template.Content; - return design; + if (string.IsNullOrEmpty(design)) + { + design = _defaultViewContentProvider.GetDefaultFileContent(template.MasterTemplateAlias); } + + return design; } } diff --git a/src/Umbraco.Core/IRegisteredObject.cs b/src/Umbraco.Core/IRegisteredObject.cs index 54ac6e1a5709..103e10dab162 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 f84d9a0a7bd9..21c6d4f0c7da 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 cfda3a396d41..88b6e23cbf5c 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 2ec741d200c5..69e28db92caa 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) - { - View = view; - ViewModel = viewModel; - } + /// + /// 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)); - /// - /// 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) - { } + /// + /// Gets the view. + /// + /// + /// The view. + /// + public string? View { get; private set; } - /// - /// 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 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) + /// + /// 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) { - if (info == null) - { - throw new ArgumentNullException(nameof(info)); - } + throw new ArgumentNullException(nameof(info)); + } - info.AddValue(nameof(View), View); + 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 1ee7f685d429..5403ded3aeb8 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) - { - _hostingEnvironment = hostingEnvironment; - _jsonSerializer = jsonSerializer; - } + public static IEnumerable GetStatus() => + new List(_steps).OrderBy(x => x.ServerOrder); + + public void Reset() + { + _steps = new ConcurrentHashSet(); + ClearFiles(); + } - 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; + } - private string GetFile(Guid installId) + public void ClearFiles() + { + var dir = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData.EnsureEndsWith('/') + + "Install/"); + if (Directory.Exists(dir)) { - var file = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "Install/" - + "install_" - + installId.ToString("N") - + ".txt"); - return file; + var files = Directory.GetFiles(dir); + foreach (var f in files) + { + File.Delete(f); + } } - - public void Reset() + else { - _steps = new ConcurrentHashSet(); - ClearFiles(); + Directory.CreateDirectory(dir); } + } - public void ClearFiles() + public IEnumerable InitializeFromFile(Guid installId) + { + // check if we have our persisted file and read it + var file = GetFile(installId); + if (File.Exists(file)) { - var dir = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "Install/"); - if (Directory.Exists(dir)) + IEnumerable? deserialized = + _jsonSerializer.Deserialize>( + File.ReadAllText(file)); + if (deserialized is not null) { - var files = Directory.GetFiles(dir); - foreach (var f in files) + 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"); + } + + return new List(_steps); + } - public IEnumerable InitializeFromFile(Guid installId) + 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 @@ public IEnumerable InitializeFromFile(Guid installId) } else { - throw new InvalidOperationException("Cannot initialize from file, the installation file with id " + installId + " does not exist"); - } - return new List(_steps); - } + ClearFiles(); - 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 - var file = GetFile(installId); - if (File.Exists(file)) + // otherwise just create the steps in memory (brand new install) + foreach (InstallSetupStep step in steps.OrderBy(x => x.ServerOrder)) { - var deserialized = _jsonSerializer.Deserialize>( - File.ReadAllText(file)); - if (deserialized is not null) - { - foreach (var item in deserialized) - { - _steps.Add(item); - } - } + _steps.Add(new InstallTrackingItem(step.Name, step.ServerOrder)); } - 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); - } + // 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) + else { - var trackingItem = _steps.Single(x => x.Name == name); - if (additionalData != null) + // ensure that the file exists with the current install id + var file = GetFile(installId); + if (File.Exists(file) == false) { - trackingItem.AdditionalData = additionalData; - } - trackingItem.IsComplete = true; + ClearFiles(); - //save the file - var file = GetFile(installId); - var serialized = _jsonSerializer.Serialize(new List(_steps)); - File.WriteAllText(file, serialized); + // 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 14d77ecb7711..40f54bab333f 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); + _filePermissionHelper = filePermissionHelper; + _localizedTextService = localizedTextService; + } - 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 }); - } + /// + public override Task ExecuteAsync(object model) + { + // validate file permissions + var permissionsOk = + _filePermissionHelper.RunFilePermissionTestSuite( + out Dictionary> report); - return Task.FromResult(null); + 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 }); } - /// - public override bool RequiresExecution(object model) => true; + 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 15286d249f53..cb008bf77c0b 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.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; + private readonly IOptions _globalSettings; + private readonly ISiteIdentifierService _siteIdentifierService; - public TelemetryIdentifierStep( - IOptions globalSettings, - 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 6530983de22f..763b69226e99 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 @@ public override object ViewModel { 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 da2f61fce5dc..eb892d9ceea3 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 9dc42553e09a..c86307d9b0d7 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 +namespace Umbraco.Cms.Core.Install.Models; + +[DataContract(Name = "installInstructions", Namespace = "")] +public class InstallInstructions { - [DataContract(Name = "installInstructions", Namespace = "")] - public class InstallInstructions - { - [DataMember(Name = "instructions")] - public IDictionary? Instructions { get; set; } + [DataMember(Name = "instructions")] + public IDictionary? Instructions { get; set; } - [DataMember(Name = "installId")] - public Guid InstallId { 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 43b3fc73fe16..650c7469986e 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 358bd922347b..2a1e3ce9f7fa 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; } + Steps = new List(); + InstallId = Guid.NewGuid(); + } - [DataMember(Name = "steps")] - public IEnumerable Steps { get; set; } + [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 15a4c12b4723..3849a09d7501 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, string view, object? viewModel = null) + { + ViewModel = viewModel; + SavedStepData = savedStepData; + View = view; + } - public InstallSetupResult(IDictionary savedStepData) - { - SavedStepData = savedStepData; - } + public InstallSetupResult(IDictionary savedStepData) => SavedStepData = savedStepData; - public InstallSetupResult(string view, object? viewModel = null) - { - ViewModel = viewModel; - View = view; - } + 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; } + /// + /// 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; 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; } - /// - /// The view model to return to the UI if this step is returning a view (optional) - /// - public object? ViewModel { get; private set; } - } + /// + /// 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 766458f99fb5..a9d24447c680 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 - { - /// - /// Defines the step model type on the server side so we can bind it - /// - [IgnoreDataMember] - public override Type StepType => typeof(T); + [IgnoreDataMember] + public override Type StepType => typeof(T); - /// - /// The step execution method - /// - /// - /// - public abstract Task ExecuteAsync(T model); + /// + /// 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); - } + /// + /// 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 +[DataContract(Name = "step", Namespace = "")] +public abstract class InstallSetupStep +{ + protected 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; } + Name = att.Name; + View = att.View; + ServerOrder = att.ServerOrder; + Description = att.Description; + InstallTypeTarget = att.InstallTypeTarget; + PerformsAppRestart = att.PerformsAppRestart; + } - [DataMember(Name = "view")] - public virtual string View { get; private set; } + [DataMember(Name = "name")] + public string Name { 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 = "view")] + public virtual string View { get; private set; } - [DataMember(Name = "description")] - public string Description { 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; } - [IgnoreDataMember] - public InstallationType InstallTypeTarget { get; private set; } + [DataMember(Name = "description")] + public string Description { get; private set; } - [IgnoreDataMember] - public bool PerformsAppRestart { get; private set; } + [IgnoreDataMember] + public InstallationType InstallTypeTarget { 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; } + [IgnoreDataMember] + public bool PerformsAppRestart { get; } - /// - /// Defines the step model type on the server side so we can bind it - /// - [IgnoreDataMember] - public abstract Type StepType { 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 7feaced0521f..c6d0657d3366 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; - - //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; } + 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; } + + 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 3a34264d77fd..74170857b5e0 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(); - } + Name = name; + ServerOrder = serverOrder; + AdditionalData = new Dictionary(); + } + + public string Name { get; set; } + + public int ServerOrder { get; set; } - public string Name { get; set; } - public int ServerOrder { get; set; } - public bool IsComplete { get; set; } - public IDictionary AdditionalData { get; set; } + public bool IsComplete { get; set; } - protected bool Equals(InstallTrackingItem other) + public IDictionary AdditionalData { get; set; } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - return string.Equals(Name, other.Name); + 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((InstallTrackingItem) obj); + return true; } - public override int GetHashCode() + if (obj.GetType() != GetType()) { - return Name.GetHashCode(); + 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 99ecf8ce1f7f..b2b6a428fa46 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 3b9a204f1077..9ac30ab9a7e3 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 d294a24c1d06..61f76c795d7a 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 3d8ab26af96d..d0bec2097fd5 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 123654bbe255..31ebcf688f5c 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 LambdaExpressionCacheKey(string returnType, string expression, params string[] argTypes) - { - ReturnType = returnType; - ExpressionAsString = expression; - ArgTypes = new HashSet(argTypes); - _toString = null; - } + public readonly HashSet ArgTypes; - public LambdaExpressionCacheKey(LambdaExpression obj) - { - ReturnType = obj.ReturnType.FullName; - ExpressionAsString = obj.ToString(); - ArgTypes = new HashSet(obj.Parameters.Select(x => x.Type.FullName)); - _toString = null; - } + public LambdaExpressionCacheKey(string returnType, string expression, params string[] argTypes) + { + ReturnType = returnType; + ExpressionAsString = expression; + ArgTypes = new HashSet(argTypes); + _toString = null; + } - /// - /// The argument type names of the - /// - public readonly HashSet ArgTypes; + 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 return type of the + /// + public readonly string? ReturnType; - /// - /// The original string representation of the - /// - public readonly string ExpressionAsString; + /// + /// The original string representation of the + /// + public readonly string ExpressionAsString; - private string? _toString; + 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)); - } + /// + /// 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) + /// + /// 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)) { - if (ReferenceEquals(obj, null)) return false; - var casted = (LambdaExpressionCacheKey)obj; - return casted.ToString() == ToString(); + return false; } - /// - /// 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(); - } + 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 a22ac75127b9..b153e096c4b8 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 { + 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) + { + _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 + + if (thresholdMilliseconds == 0) + { + 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); + } + + 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(); + /// - /// 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. + /// Reports a failure. /// - public class DisposableTimer : DisposableObjectSlim + /// 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) { - 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; + _failed = true; + _failMessage = failMessage ?? _failMessage ?? "Failed."; + _failException = exception; + } - // 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) - { - _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 + /// + /// Disposes resources. + /// + /// Overrides abstract class which handles required locking. + protected override void DisposeResources() + { + Stopwatch.Stop(); + + _profilerStep?.Dispose(); - if (thresholdMilliseconds == 0) + 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(); + break; - 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 34e4d702c6b2..662ee7891c84 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 99d88ce926fb..252f91aaa532 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 4b2bf6fc4844..ab580d6aaead 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 { - /// - /// Defines the profiling service. + /// Gets an that will time the code between its creation and disposal. /// - 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); + /// The name of the step. + /// A step. + /// The returned is meant to be used within a using (...) {{ ... }} block. + IDisposable? Step(string name); - /// - /// Starts the profiler. - /// - 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 30812fc1563e..806ee54e7a6d 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 587361998858..92c4d55f0c86 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 c9e1b09e0880..2981dd598708 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 - { - static readonly string RequestIdItemName = typeof(LogHttpRequest).Name + "+RequestId"; + 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) - { - 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 9e120023248e..b7271ecf043a 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 1f4b4bbe9094..0504a2a1ae00 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) - { - _logger = logger; - } + /// + public void Start() + { + // the log will always be started + } - /// - public IDisposable Step(string name) - { - _logger.LogDebug("Begin: {ProfileName}", name); - return new LightDisposableTimer(duration => _logger.LogInformation("End {ProfileName} ({ProfileDuration}ms)", name, duration)); - } + /// + public void Stop(bool discardResults = false) + { + // the log never stops + } - /// - public void Start() - { - // the log will always be started - } + // a lightweight disposable timer + private class LightDisposableTimer : DisposableObjectSlim + { + private readonly Action _callback; + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); - /// - public void Stop(bool discardResults = false) + protected internal LightDisposableTimer(Action callback) { - // the log never stops + _callback = callback ?? throw new ArgumentNullException(nameof(callback)); } - // a lightweight disposable timer - private class LightDisposableTimer : DisposableObjectSlim + protected override void DisposeResources() { - 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 f191af302314..d2a24d24a95a 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 5a6f995dfa99..950e9bb8f46d 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) - { - 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 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, - /// - /// 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); - } + // 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), - private static void LogErrorsInner(Task task, Action logAction) + // 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) { - 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 89a0307515ec..821728c7a6ec 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 VoidDisposable(); + private readonly VoidDisposable _disposable = new(); - public IDisposable Step(string name) - { - return _disposable; - } + public IDisposable Step(string name) => _disposable; - public void Start() - { } + public void Start() + { + } - public void Stop(bool discardResults = false) - { } + public void Stop(bool discardResults = false) + { + } - private class VoidDisposable : DisposableObjectSlim + private class VoidDisposable : DisposableObjectSlim + { + protected override void DisposeResources() { - protected override void DisposeResources() - { } } } } diff --git a/src/Umbraco.Core/Logging/ProfilerExtensions.cs b/src/Umbraco.Core/Logging/ProfilerExtensions.cs index 67739c2f381d..e69506702af9 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) + { + throw new ArgumentNullException(nameof(profiler)); + } + + if (reporting == 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(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 d3388bda01d0..997f139539ae 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); + 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; } - #endregion - } + /// + /// 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 bac3d36268a9..f1e7d8c38320 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 7998b00fd7fc..c36c63016804 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 b3c505682a99..49a53f11b05c 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 5242b14d863d..12649bf91c74 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; } + public MacroModel(IMacro? macro) + { + if (macro == null) + { + return; + } - /// - /// The Macro Alias - /// - public string? Alias { get; set; } + Id = macro.Id; + Name = macro.Name; + Alias = macro.Alias; + MacroSource = macro.MacroSource; + CacheDuration = macro.CacheDuration; + CacheByPage = macro.CacheByPage; + CacheByMember = macro.CacheByMember; + RenderInEditor = macro.UseInEditor; - public string? MacroSource { get; set; } + foreach (IMacroProperty prop in macro.Properties) + { + Properties.Add(new MacroPropertyModel(prop.Alias, string.Empty, prop.EditorAlias)); + } + } - public int CacheDuration { get; set; } + /// + /// The Macro Id + /// + public int Id { get; set; } - public bool CacheByPage { get; set; } + /// + /// The Macro Name + /// + public string? Name { get; set; } - public bool CacheByMember { get; set; } + /// + /// The Macro Alias + /// + public string? Alias { get; set; } - public bool RenderInEditor { get; set; } + public string? MacroSource { get; set; } - public string? CacheIdentifier { get; set; } + public int CacheDuration { get; set; } - public List Properties { get; } = new List(); + public bool CacheByPage { get; set; } - public MacroModel() - { } + public bool CacheByMember { get; set; } - public MacroModel(IMacro macro) - { - if (macro == null) 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; - - foreach (var prop in macro.Properties) - Properties.Add(new MacroPropertyModel(prop.Alias, string.Empty, prop.EditorAlias)); - } - } + 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 643d154f2139..c1022c35613e 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 string Key { get; set; } + public MacroPropertyModel() => Key = string.Empty; - public string? Value { get; set; } + public MacroPropertyModel(string key, string value) + { + Key = key; + Value = value; + } - public string? Type { get; set; } + public MacroPropertyModel(string key, string value, string type) + { + Key = key; + Value = value; + Type = type; + } - public MacroPropertyModel() - { - Key = string.Empty; - } + public string Key { get; set; } - public MacroPropertyModel(string key, string value) - { - Key = key; - Value = value; - } + public string? Value { get; set; } - public MacroPropertyModel(string key, string value, string type) - { - Key = key; - Value = value; - Type = type; - } - } + public string? Type { get; set; } } diff --git a/src/Umbraco.Core/Mail/IEmailSender.cs b/src/Umbraco.Core/Mail/IEmailSender.cs index 0c573c542ccc..2eb8cc826358 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 885ad89da2bf..3c09bdc7e632 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 15e36767d95c..5b1fa0923ae6 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 0cb5016a1b6b..b3901d5ab96d 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 810efb6c455f..fe04c205d9ff 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 939d635fc31f..5e41681ea608 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 0984f1a889fd..d2998a083920 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 09d3ccbe1ccb..f8b29e9f561b 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 39e487823385..ba911b183cbd 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 6532e2f63d06..2bd84a1bddf4 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 ed44742bc043..5bfc2a740ef3 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; - - /// - /// 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; } + [DataMember(Name = "name")] + public string? Name { get; set; } - /// - /// Gets or sets the weight of the content app. - /// - [DataMember(Name = "weight")] - public int Weight { 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 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 weight of the content app. + /// + [DataMember(Name = "weight")] + public int Weight { get; set; } - /// - /// Gets or sets the view for rendering the content app. - /// - [DataMember(Name = "view")] - public string? View { 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 list of 'show' conditions for the content app. - /// - [DataMember(Name = "show")] - public string[] Show { get; set; } = Array.Empty(); + /// + /// 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(); } diff --git a/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs index c4bc87e9a23b..122ecc1cb7d5 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.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. - // ] - // }, - // ... - // ] - - /// - /// Represents a content app factory, for content apps parsed from the manifest. - /// - public class ManifestContentAppFactory : IContentAppFactory - { - private readonly ManifestContentAppDefinition _definition; - private readonly IIOHelper _ioHelper; + private readonly ManifestContentAppDefinition _definition; + private readonly IIOHelper _ioHelper; - public ManifestContentAppFactory(ManifestContentAppDefinition definition, IIOHelper ioHelper) - { - _definition = definition; - _ioHelper = ioHelper; - } + private ContentApp? _app; + private ShowRule[]? _showRules; - private ContentApp? _app; - private ShowRule[]? _showRules; - - /// - public ContentApp? GetContentAppFor(object o,IEnumerable userGroups) - { - string? partA, partB; - - switch (o) - { - case IContent content: - partA = "content"; - partB = content.ContentType.Alias; - break; + public ManifestContentAppFactory(ManifestContentAppDefinition definition, IIOHelper ioHelper) + { + _definition = definition; + _ioHelper = ioHelper; + } - 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; + /// + public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) + { + string? partA, partB; - default: - return null; - } + switch (o) + { + 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(); + ShowRule[] rules = _showRules ??= ShowRule.Parse(_definition.Show).ToArray(); + var userGroupsList = userGroups.ToList(); - var okRole = false; - var hasRole = false; - var okType = false; - var hasType = false; + var okRole = false; + var hasRole = false; + var okType = false; + var hasType = false; - foreach (var rule in rules) + foreach (ShowRule rule in rules) + { + if (rule.PartA?.InvariantEquals("role") ?? false) { - 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 - { - // if type has been ok-ed already, skip the rule - if (okType) - continue; - // remember we have type rules - hasType = true; + // remember we have role rules + hasRole = true; - // if the entry does not apply, skip it - if (!rule.Matches(partA, partB)) + foreach (IReadOnlyUserGroup 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 type rules are ok - okType = true; + // else ok to display, remember roles are ok, break from userGroupsList + okRole = rule.Show; + break; } } - // 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 + // it is a type rule + else { - Alias = _definition.Alias, - Name = _definition.Name, - Icon = _definition.Icon, - View = _ioHelper.ResolveRelativeOrVirtualUrl(_definition.View), - Weight = _definition.Weight - }; + // 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; + } } - private class ShowRule + // 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)) { - private static readonly Regex ShowRegex = new Regex("^([+-])?([a-z]+)/([a-z0-9_]+|\\*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + return null; + } - public bool Show { get; private set; } - public string? PartA { get; private set; } - public string? PartB { get; private set; } + // 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, + }; + } - public bool Matches(string? partA, string? partB) - { - return (PartA == "*" || (PartA?.InvariantEquals(partA) ?? false)) && (PartB == "*" || (PartB?.InvariantEquals(partB) ?? false)); - } + 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 static IEnumerable Parse(string[] rules) + public string? PartB { get; private set; } + + public static IEnumerable Parse(string[] rules) + { + foreach (var rule in rules) { - foreach (var rule in rules) + Match match = ShowRegex.Match(rule); + if (!match.Success) { - 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 - }; + 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 a10c3a217798..75cdf24ebe21 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 9c692f69b342..a1d5cac0c1b0 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 +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Contains the manifest filters. +/// +public class ManifestFilterCollection : BuilderCollectionBase { + public ManifestFilterCollection(Func> items) + : base(items) + { + } + /// - /// Contains the manifest filters. + /// Filters package manifests. /// - public class ManifestFilterCollection : BuilderCollectionBase + /// The package manifests. + public void Filter(List manifests) { - 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 00ac3609dd27..5f012d10c9db 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 +namespace Umbraco.Cms.Core.Manifest; + +public class ManifestFilterCollectionBuilder : OrderedCollectionBuilderBase { - public class ManifestFilterCollectionBuilder : OrderedCollectionBuilderBase - { - protected override ManifestFilterCollectionBuilder This => this; + protected override ManifestFilterCollectionBuilder This => this; - // do NOT cache this, it's only used once - protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; - } + // 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 864a0734e266..c7671c91e25e 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 +namespace Umbraco.Cms.Core.Manifest; + +[DataContract(Name = "section", Namespace = "")] +public class ManifestSection : ISection { - [DataContract(Name = "section", Namespace = "")] - public class ManifestSection : ISection - { - [DataMember(Name = "alias")] - public string Alias { get; set; } = string.Empty; + [DataMember(Name = "alias")] + public string Alias { get; set; } = string.Empty; - [DataMember(Name = "name")] - public string Name { 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 a71cf1f6f663..7bf07cfde9c3 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; - } - [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(); + if (!Source.IsNullOrWhiteSpace()) + { + _packageName = Path.GetFileName(Path.GetDirectoryName(Source)); + } + + return _packageName; + } + 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 3d4270c93e04..db836fa3b821 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 c99359cbdf6e..5cbee7164c3d 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. - /// 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. - /// 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. - /// 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 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. - /// 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. - /// 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 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. - /// A list containing the target objects. - List MapEnumerable(IEnumerable source, MapperContext context); - } + /// + /// 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 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); + + /// + /// 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. + /// 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. + /// 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 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. + /// 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 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); } diff --git a/src/Umbraco.Core/Mapping/MapDefinitionCollection.cs b/src/Umbraco.Core/Mapping/MapDefinitionCollection.cs index 27d4ad73d004..db35a3ffac0c 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 698dce1648e1..1ac6de5b33e2 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 +namespace Umbraco.Cms.Core.Mapping; + +public class MapDefinitionCollectionBuilder : SetCollectionBuilderBase { - public class MapDefinitionCollectionBuilder : SetCollectionBuilderBase - { - protected override MapDefinitionCollectionBuilder This => this; + protected override MapDefinitionCollectionBuilder This => this; - protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; - } + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; } diff --git a/src/Umbraco.Core/Mapping/MapperContext.cs b/src/Umbraco.Core/Mapping/MapperContext.cs index 2355e9bd05cb..ef8663beeb82 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 - { - private readonly IUmbracoMapper _mapper; - private IDictionary? _items; + public MapperContext(IUmbracoMapper mapper) => _mapper = mapper; - /// - /// 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 a value indicating whether the context has items. + /// + public bool HasItems => _items != null; - /// - /// Gets the context items. - /// - public IDictionary Items => _items ?? (_items = new Dictionary()); + /// + /// Gets the context items. + /// + public IDictionary Items => _items ??= new Dictionary(); - #region Map + #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); + /// + /// 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); - } - */ + // 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); + /// + /// 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); - } - */ + // 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); + /// + /// 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); - } - */ + // 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()!; - } + /// + /// 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 - } + #endregion } diff --git a/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs b/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs index b79e1a8de290..3ea329abec6f 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 6d745d3d4914..e51005b84bd6 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 615d16f51c4c..655d68b87881 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 f79880b61f5c..121785d7ebe3 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 2ea5fd8109ac..5e11780645db 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 2e0ea7864932..53d13cc063d7 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 36df7e736212..4adb02f8fbe0 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 1d6bed791bf6..2fdadee6ea42 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 89179d40af16..ded01ef0d95e 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 e9ada74cf690..4e3f5b6731ef 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 f79e78b8b3f6..95330a646781 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 string ApiEndpoint => "https://embed.lottiefiles.com/oembed"; + public override Dictionary RequestParams => new(); - public override string[] UrlSchemeRegex => new string[] + 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) { - @"lottiefiles\.com/*" - }; - public override Dictionary RequestParams => new Dictionary(); + return null; + } - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + 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 { - 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 031105033b4f..b09baba0dbd2 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 -{ - public abstract class OEmbedProviderBase : IEmbedProvider - { - private readonly IJsonSerializer _jsonSerializer; +namespace Umbraco.Cms.Core.Media.EmbedProviders; - protected OEmbedProviderBase(IJsonSerializer jsonSerializer) - { - _jsonSerializer = jsonSerializer; - } +public abstract class OEmbedProviderBase : IEmbedProvider +{ + private static HttpClient? _httpClient; + private readonly IJsonSerializer _jsonSerializer; - private static HttpClient? _httpClient; + protected OEmbedProviderBase(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; - public abstract string ApiEndpoint { get; } + public abstract string ApiEndpoint { get; } - public abstract string[] UrlSchemeRegex { get; } + public abstract string[] UrlSchemeRegex { get; } - public abstract Dictionary RequestParams { get; } + public abstract Dictionary RequestParams { get; } - public abstract string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0); + public abstract string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0); - public virtual string GetEmbedProviderUrl(string url, int maxWidth, int maxHeight) + public virtual string GetEmbedProviderUrl(string url, int maxWidth, int maxHeight) + { + if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) == false) { - 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}"); + throw new ArgumentException("Not a valid URL.", nameof(url)); + } - if (maxWidth > 0) - fullUrl.Append("&maxwidth=" + maxWidth); + var fullUrl = new StringBuilder(); - if (maxHeight > 0) - fullUrl.Append("&maxheight=" + maxHeight); + fullUrl.Append(ApiEndpoint); + fullUrl.Append("?url=" + WebUtility.UrlEncode(url)); - return fullUrl.ToString(); + foreach (KeyValuePair param in RequestParams) + { + 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 f003aa841cec..370d2609c79f 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 { - /// - /// Wrapper class for OEmbed response - /// - [DataContract] - public class OEmbedResponse - { - [DataMember(Name ="type")] - public string? Type { get; set; } + [DataMember(Name = "type")] + public string? Type { get; set; } - [DataMember(Name ="version")] - public string? Version { get; set; } + [DataMember(Name = "version")] + public string? Version { get; set; } - [DataMember(Name ="title")] - public string? Title { get; set; } + [DataMember(Name = "title")] + public string? Title { get; set; } - [DataMember(Name ="author_name")] - public string? AuthorName { get; set; } + [DataMember(Name = "author_name")] + public string? AuthorName { get; set; } - [DataMember(Name ="author_url")] - public string? AuthorUrl { get; set; } + [DataMember(Name = "author_url")] + public string? AuthorUrl { get; set; } - [DataMember(Name ="provider_name")] - public string? ProviderName { get; set; } + [DataMember(Name = "provider_name")] + public string? ProviderName { get; set; } - [DataMember(Name ="provider_url")] - public string? ProviderUrl { get; set; } + [DataMember(Name = "provider_url")] + public string? ProviderUrl { get; set; } - [DataMember(Name ="thumbnail_url")] - public string? ThumbnailUrl { get; set; } + [DataMember(Name = "thumbnail_url")] + public string? ThumbnailUrl { get; set; } - [DataMember(Name ="thumbnail_height")] - public double? ThumbnailHeight { get; set; } + [DataMember(Name = "thumbnail_height")] + public double? ThumbnailHeight { get; set; } - [DataMember(Name ="thumbnail_width")] - public double? ThumbnailWidth { get; set; } + [DataMember(Name = "thumbnail_width")] + public double? ThumbnailWidth { get; set; } - [DataMember(Name ="html")] - public string? Html { get; set; } + [DataMember(Name = "html")] + public string? Html { get; set; } - [DataMember(Name ="url")] - public string? Url { get; set; } + [DataMember(Name = "url")] + public string? Url { get; set; } - [DataMember(Name ="height")] - public double? Height { get; set; } + [DataMember(Name = "height")] + public double? Height { get; set; } - [DataMember(Name ="width")] - public double? Width { get; set; } + [DataMember(Name = "width")] + public double? Width { get; set; } - /// - /// Gets the HTML. - /// - /// The response HTML - public string GetHtml() + /// + /// 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 42e500aa5c03..f00e631d2552 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 687da98697e4..f3d4e2caaef2 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 511cbf012d21..9c8a607e13a2 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 934ec4b5c102..555224032a1c 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 db324bda1207..ed3990ba4de2 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 3888462dbc03..594c7ead8325 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 6afc6e4308a3..f6cc50f80103 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 + LittleEndian = 1, + BigEndian = 2, + } - #region Member Variables - private ByteOrder mFrom, mTo; - #endregion + #endregion - #region Constructors - public BitConverterEx(ByteOrder from, ByteOrder to) - { - mFrom = from; - mTo = to; - } - #endregion + #region Member Variables - #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 + private readonly ByteOrder mFrom; + private readonly ByteOrder mTo; - #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); - } - } + #endregion - /// - /// 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); - } - } + #region Constructors - /// - /// 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 + public BitConverterEx(ByteOrder from, ByteOrder to) + { + mFrom = from; + mTo = to; + } - #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); - } + #endregion - /// - /// 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); - } + #region Properties - /// - /// 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); - } + /// + /// Indicates the byte order in which data is stored in this platform. + /// + public static ByteOrder SystemByteOrder => + BitConverter.IsLittleEndian ? ByteOrder.LittleEndian : ByteOrder.BigEndian; - /// - /// 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); - } + #endregion - /// - /// 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); - } + #region Predefined Values - /// - /// 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); - } + /// + /// Returns a bit converter that converts between little-endian and system byte-order. + /// + public static BitConverterEx LittleEndian => new BitConverterEx(ByteOrder.LittleEndian, SystemByteOrder); - /// - /// 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); - } + /// + /// Returns a bit converter that converts between big-endian and system byte-order. + /// + public static BitConverterEx BigEndian => new BitConverterEx(ByteOrder.BigEndian, SystemByteOrder); - /// - /// 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); - } + /// + /// Returns a bit converter that does not do any byte-order conversion. + /// + public static BitConverterEx SystemEndian => new BitConverterEx(SystemByteOrder, SystemByteOrder); - /// - /// 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); - } + #endregion - /// - /// 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; - } + #region Static Methods - /// - /// 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 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 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 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 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 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 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 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 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 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 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 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 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 + /// + /// 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); + } - #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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 16-bit unsigned integer to an array of bytes. - /// - public byte[] GetBytes(ushort value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } + /// + /// 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; + } - /// - /// Converts the given 32-bit unsigned integer to an array of bytes. - /// - public byte[] GetBytes(uint value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } + #endregion - /// - /// Converts the given 64-bit unsigned integer to an array of bytes. - /// - public byte[] GetBytes(ulong value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } + #region Instance Methods - /// - /// 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 array of bytes to a 16-bit unsigned integer. + /// + public char ToChar(byte[] value, long startIndex) => ToChar(value, startIndex, 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 array of bytes to a 16-bit unsigned integer. + /// + public ushort ToUInt16(byte[] value, long startIndex) => ToUInt16(value, startIndex, 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 array of bytes to a 32-bit unsigned integer. + /// + public uint ToUInt32(byte[] value, long startIndex) => ToUInt32(value, startIndex, 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 array of bytes to a 64-bit unsigned integer. + /// + public ulong ToUInt64(byte[] value, long startIndex) => ToUInt64(value, startIndex, 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 + /// + /// Converts the given array of bytes to a 16-bit signed integer. + /// + public short ToInt16(byte[] value, long startIndex) => ToInt16(value, startIndex, mFrom, mTo); - #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; - } + /// + /// 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); - /// - /// Reverse the array of bytes as needed. - /// - private static byte[] CheckData(byte[] value, ByteOrder from, ByteOrder to) + /// + /// 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) { - return CheckData(value, 0, value.Length, from, to); + Array.Reverse(data); } - #endregion + + 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 74465a66840d..e8dc7c4eb993 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 + #endregion - #region Static Methods - /// - /// Returns an ASCII string converted from the given byte array. - /// - public static string ToAscii(byte[] data, bool endatfirstnull, Encoding encoding) + #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) { - int len = data.Length; - if (endatfirstnull) + len = Array.IndexOf(data, (byte)0); + if (len == -1) { - len = Array.IndexOf(data, (byte)0); - if (len == -1) len = data.Length; + 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); - } + return encoding.GetString(data, 0, len); + } - /// - /// Returns a string converted from the given byte array. - /// from the numeric value of each byte. - /// - public static string ToString(byte[] data) + /// + /// 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) { - StringBuilder sb = new StringBuilder(); - foreach (byte b in data) - sb.Append(b); - return sb.ToString(); + sb.Append(b); } - /// - /// Returns a DateTime object converted from the given byte array. - /// - public static DateTime ToDateTime(byte[] data, bool hastime) + 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 { - 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) + if (hastime && parts.Length == 6) { - return DateTime.MinValue; + // 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)); } - catch (ArgumentException) + + if (!hastime && parts.Length == 3) { - return DateTime.MinValue; + // yyyy:MM:dd + return new DateTime( + int.Parse(parts[0], CultureInfo.InvariantCulture), + int.Parse(parts[1], CultureInfo.InvariantCulture), + int.Parse(parts[2], CultureInfo.InvariantCulture)); } - } - /// - /// Returns a DateTime object converted from the given byte array. - /// - public static DateTime ToDateTime(byte[] data) - { - return ToDateTime(data, true); + return DateTime.MinValue; } - - /// - /// 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) + catch (ArgumentOutOfRangeException) { - 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)); + return DateTime.MinValue; } - - /// - /// 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) + catch (ArgumentException) { - 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)); + return DateTime.MinValue; } + } - /// - /// 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 a DateTime object converted from the given byte array. + /// + public static DateTime ToDateTime(byte[] data) => ToDateTime(data, true); - /// - /// 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 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 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) + /// + /// 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++) { - 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; + var num = new byte[2]; + Array.Copy(data, i * 2, num, 0, 2); + numbers[i] = ToUInt16(num, 0, frombyteorder, SystemByteOrder); } - /// - /// 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) + 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++) { - 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; + var num = new byte[4]; + Array.Copy(data, i * 4, num, 0, 4); + numbers[i] = ToUInt32(num, 0, frombyteorder, SystemByteOrder); } - /// - /// 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) + 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++) { - 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; + var num = new byte[4]; + Array.Copy(data, i * 4, num, 0, 4); + numbers[i] = ToInt32(num, 0, byteorder, SystemByteOrder); } - /// - /// 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) + 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++) { - if (addnull) value += '\0'; - return encoding.GetBytes(value); + 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)); } - /// - /// Converts the given ascii string to an array of bytes without adding a null terminator. - /// - public static byte[] GetBytes(string value, Encoding encoding) + 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++) { - return GetBytes(value, false, encoding); + 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)); } - /// - /// Converts the given datetime to an array of bytes with a null terminator. - /// - public static byte[] GetBytes(DateTime value, bool hastime) + 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) { - 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); + value += '\0'; } - /// - /// 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) + 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) { - 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; + str = value.ToString("yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture); } - - /// - /// 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) + else { - 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; + str = value.ToString("yyyy:MM:dd", CultureInfo.InvariantCulture); } - /// - /// 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) + 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++) { - 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; + var num = GetBytes(value[i], SystemByteOrder, tobyteorder); + Array.Copy(num, 0, data, i * 2, 2); } - /// - /// 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) + 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++) { - 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; + var num = GetBytes(value[i], SystemByteOrder, tobyteorder); + Array.Copy(num, 0, data, i * 4, 4); } - /// - /// 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) + 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++) { - 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; + var num = GetBytes(value[i], SystemByteOrder, tobyteorder); + Array.Copy(num, 0, data, i * 4, 4); } - /// - /// 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) + 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++) { - 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; + 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); } - /// - /// 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) + 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++) { - 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; + 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); } - #endregion + + return data; } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/ExifEnums.cs b/src/Umbraco.Core/Media/Exif/ExifEnums.cs index 1ce0ec4891ef..5e27b8dd2418 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 -{ - 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, - } +namespace Umbraco.Cms.Core.Media.Exif; + +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, } diff --git a/src/Umbraco.Core/Media/Exif/ExifExceptions.cs b/src/Umbraco.Core/Media/Exif/ExifExceptions.cs index 3d0472c100b3..bd4426e15af5 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 message that describes the error. + public NotValidExifFileException(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. - protected NotValidExifFileException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// 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 9aa62f4ea392..ffa31f0cc159 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) - { - mValue = value; - mIsBitField = isbitfield; - } + protected override object _Value + { + get => Value; + set => Value = (T)value; + } + + public bool IsBitField => mIsBitField; - public ExifEnumProperty(ExifTag tag, T value) - : this(tag, value, false) + public override ExifInterOperability Interoperability + { + get { + var tagid = ExifTagFactory.GetTagID(mTag); - } + Type type = typeof(T); + Type basetype = Enum.GetUnderlyingType(type); - public override ExifInterOperability Interoperability - { - get + if (type == typeof(FileSource) || type == typeof(SceneType)) + { + // 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)) { - 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}"); + // 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; } } + mValue = value; + Encoding = encoding; + } - static public implicit operator string(ExifEncodedString obj) { return obj.mValue; } + public new string Value + { + get => mValue; + set => mValue = value; + } - public override string ToString() { return mValue; } + protected override object _Value + { + get => Value; + set => Value = (string)value; + } - public ExifEncodedString(ExifTag tag, string value, Encoding encoding) - : base(tag) - { - mValue = value; - mEncoding = encoding; - } + public Encoding Encoding { get; set; } - public override ExifInterOperability Interoperability + public override ExifInterOperability Interoperability + { + get { - get + 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") { - 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); + enc = "Unicode\0"; } + else + { + 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 an ASCII string formatted as DateTime. (EXIF Specification: ASCII) Used for the date time fields. - /// - internal class ExifDateTime : ExifProperty - { - protected DateTime mValue; - protected override object _Value { get { return Value; } set { Value = (DateTime)value; } } - public new DateTime Value { get { return mValue; } set { mValue = value; } } + public static implicit operator string(ExifEncodedString obj) => obj.mValue; - static public implicit operator DateTime(ExifDateTime obj) { return obj.mValue; } + public override string ToString() => mValue; +} - public override string ToString() { return mValue.ToString("yyyy.MM.dd HH:mm:ss"); } +/// +/// 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 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)); - } - } + public new DateTime Value + { + 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 = (DateTime)value; + } + + public override ExifInterOperability Interoperability => + new(ExifTagFactory.GetTagID(mTag), 2, 20, ExifBitConverter.GetBytes(mValue, true)); - public ExifVersion(ExifTag tag, string value) - : base(tag) + 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) + { + if (value.Length > 4) { - if (value.Length > 4) - mValue = value.Substring(0, 4); - else if (value.Length < 4) - mValue = value + new string(' ', 4 - value.Length); - else - mValue = value; + mValue = value[..4]; } - - public override string ToString() + else if (value.Length < 4) { - return mValue; + mValue = value + new string(' ', 4 - value.Length); } - - public override ExifInterOperability Interoperability + else { - 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); - } - } + mValue = value; } } - /// - /// 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 new string 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; } } + get => mValue; + set => mValue = value[..4]; + } - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.AppendFormat("({0:d}, {1:d})", mValue[0], mValue[1]); - return sb.ToString(); - } + protected override object _Value + { + get => Value; + set => Value = (string)value; + } - public ExifPointSubjectArea(ExifTag tag, ushort[] value) - : base(tag, value) + 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)); + } + + 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 ExifPointSubjectArea(ExifTag tag, ushort x, ushort y) - : base(tag, new ushort[] {x, y}) - { + 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) + { } - /// - /// 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 ExifPointSubjectArea(ExifTag tag, ushort x, ushort y) + : base(tag, new[] { x, y }) { - 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 ushort X + { + get => mValue[0]; + set => mValue[0] = value; + } - public ExifCircularSubjectArea(ExifTag tag, ushort[] value) - : base(tag, value) - { + protected new ushort[] Value + { + get => mValue; + set => mValue = value; + } - } + public ushort Y + { + get => mValue[1]; + set => mValue[1] = value; + } - public ExifCircularSubjectArea(ExifTag tag, ushort x, ushort y, ushort d) - : base(tag, new ushort[] { x, y, d }) - { + 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) + { } - /// - /// 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 ExifCircularSubjectArea(ExifTag tag, ushort x, ushort y, ushort d) + : base(tag, new[] { x, y, d }) { - 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() - { - 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(); - } + public ushort Diamater + { + get => mValue[2]; + set => mValue[2] = value; + } - public ExifRectangularSubjectArea(ExifTag tag, ushort[] value) - : base(tag, 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 ushort[] { x, y, w, h }) - { + 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; } - /// - /// Represents GPS latitudes and longitudes (EXIF Specification: 3xRATIONAL) - /// - internal class GPSLatitudeLongitude : ExifURationalArray + public ushort Height { - 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; } } + get => mValue[3]; + set => mValue[3] = 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() + { + 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(); + } +} - public override string ToString() - { - return string.Format("{0:F2}°{1:F2}'{2:F2}\"", (float)Degrees, (float)Minutes, (float)Seconds); - } +/// +/// 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, 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; + } - 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) }) - { + protected new MathEx.UFraction32[] Value + { + get => mValue; + set => mValue = value; + } - } + public MathEx.UFraction32 Minutes + { + get => mValue[1]; + set => mValue[1] = value; } - /// - /// Represents a GPS time stamp as UTC (EXIF Specification: 3xRATIONAL) - /// - internal class GPSTimeStamp : ExifURationalArray + public MathEx.UFraction32 Seconds { - 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; } } + get => 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 static explicit operator float(GPSLatitudeLongitude obj) => obj.ToFloat(); - public GPSTimeStamp(ExifTag tag, MathEx.UFraction32[] value) - : base(tag, value) - { + 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); +} - 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 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) }) + { } - /// - /// Represents an ASCII string. (EXIF Specification: BYTE) - /// Used by Windows XP. - /// - internal class WindowsByteString : ExifProperty + public MathEx.UFraction32 Hour { - protected string mValue; - protected override object _Value { get { return Value; } set { Value = (string)value; } } - public new string Value { get { return mValue; } set { mValue = value; } } + get => mValue[0]; + set => mValue[0] = value; + } - static public implicit operator string(WindowsByteString obj) { return obj.mValue; } + protected new MathEx.UFraction32[] Value + { + get => mValue; + set => mValue = value; + } - public override string ToString() { return mValue; } + public MathEx.UFraction32 Minute + { + get => mValue[1]; + set => mValue[1] = value; + } - public WindowsByteString(ExifTag tag, string value) - : base(tag) - { - mValue = 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 override ExifInterOperability Interoperability + public new string Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (string)value; + } + + public override ExifInterOperability Interoperability + { + get { - get - { - byte[] data = Encoding.Unicode.GetBytes(mValue); - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)data.Length, data); - } + 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 61d6b70f308b..a07ade99632f 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 { + public ExifFileTypeDescriptionProvider() + : this(TypeDescriptor.GetProvider(typeof(ImageFile))) + { + } + + public ExifFileTypeDescriptionProvider(TypeDescriptionProvider parent) + : base(parent) + { + } + /// - /// Provides a custom type descriptor for an ExifFile instance. + /// Gets a custom type descriptor for the given type and object. /// - internal sealed class ExifFileTypeDescriptionProvider : TypeDescriptionProvider - { - public ExifFileTypeDescriptionProvider() - : this(TypeDescriptor.GetProvider(typeof(ImageFile))) - { - } + /// 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); +} - public ExifFileTypeDescriptionProvider(TypeDescriptionProvider parent) - : base(parent) - { - } +/// +/// Expands ExifProperty objects contained in an ExifFile as separate properties. +/// +internal sealed class ExifFileTypeDescriptor : CustomTypeDescriptor +{ + private readonly ImageFile? owner; - /// - /// 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 ExifFileTypeDescriptor(ICustomTypeDescriptor? parent, object? instance) + : base(parent) => + owner = (ImageFile?)instance; + + public override PropertyDescriptorCollection GetProperties(Attribute[]? attributes) => GetProperties(); /// - /// Expands ExifProperty objects contained in an ExifFile as separate properties. + /// Returns a collection of property descriptors for the object represented by this type descriptor. /// - internal sealed class ExifFileTypeDescriptor : CustomTypeDescriptor + /// + /// 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) - { - 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() + if (owner is not null) { - // 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); - } + var pd = new ExifPropertyDescriptor(prop); + properties.Add(pd); } - - // Finally return the list - return new PropertyDescriptorCollection(properties.ToArray(), true); } + + // 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; - } +internal sealed class ExifPropertyDescriptor : PropertyDescriptor +{ + private readonly ExifProperty linkedProperty; + private readonly object originalValue; - public override bool CanResetValue(object component) - { - return true; - } + public ExifPropertyDescriptor(ExifProperty property) + : base(property.Name, new Attribute[] { new BrowsableAttribute(true) }) + { + linkedProperty = property; + originalValue = property.Value; + } - public override Type ComponentType - { - get { return typeof(JPEGFile); } - } + public override Type ComponentType => typeof(JPEGFile); - public override object GetValue(object? component) - { - return linkedProperty.Value; - } + public override bool IsReadOnly => false; - public override bool IsReadOnly - { - get { return false; } - } + public override Type PropertyType => linkedProperty.Value.GetType(); - public override Type PropertyType - { - get { return linkedProperty.Value.GetType(); } - } + public override bool CanResetValue(object component) => true; - public override void ResetValue(object component) - { - linkedProperty.Value = originalValue; - } + public override object GetValue(object? component) => linkedProperty.Value; - public override void SetValue(object? component, object? value) - { - if (value is not null) - { - linkedProperty.Value = value; - } - } + public override void ResetValue(object component) => linkedProperty.Value = originalValue; - public override bool ShouldSerializeValue(object component) + public override void SetValue(object? component, object? value) + { + if (value is not null) { - return false; + 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 160ee3863600..d2d9f8be6afa 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 { + public ExifInterOperability(ushort tagid, ushort typeid, uint count, byte[] data) + { + TagID = tagid; + TypeID = typeid; + Count = count; + Data = data; + } + /// - /// Represents interoperability data for an exif tag in the platform byte order. + /// Gets the tag ID defined in the Exif standard. /// - internal struct ExifInterOperability - { - private ushort mTagID; - private ushort mTypeID; - private uint mCount; - private byte[] mData; + public ushort TagID { get; } - /// - /// 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); - } + /// + /// 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; } - public ExifInterOperability(ushort tagid, ushort typeid, uint count, byte[] data) - { - mTagID = tagid; - mTypeID = typeid; - mCount = count; - mData = data; - } - } + /// + /// 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 a3c28aabbc91..6d742bb3ba6a 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 { + protected IFD mIFD; + protected string? mName; + protected ExifTag mTag; + + public ExifProperty(ExifTag tag) + { + mTag = tag; + mIFD = ExifTagFactory.GetTagIFD(tag); + } + /// - /// Represents the abstract base class for an Exif property. + /// Gets the Exif tag associated with this property. /// - internal abstract class ExifProperty - { - 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 + 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 + { + get { - get - { - if (string.IsNullOrEmpty(mName)) - return ExifTagFactory.GetTagName(mTag); - else - return mName; - } - set + if (string.IsNullOrEmpty(mName)) { - mName = value; + return ExifTagFactory.GetTagName(mTag); } + + return mName; } - 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); - } + set => mName = value; } /// - /// Represents an 8-bit unsigned integer. (EXIF Specification: BYTE) + /// Gets or sets the value of this property. /// - internal class ExifByte : ExifProperty + public object Value { - 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; } } + get => _Value; + set => _Value = value; + } - static public implicit operator byte(ExifByte obj) { return obj.mValue; } + protected abstract object _Value { get; set; } - public override string ToString() { return mValue.ToString(); } + /// + /// Gets interoperability data for this property. + /// + public abstract ExifInterOperability Interoperability { get; } +} - public ExifByte(ExifTag tag, byte value) - : base(tag) - { - mValue = value; - } +/// +/// Represents an 8-bit unsigned integer. (EXIF Specification: BYTE) +/// +internal class ExifByte : ExifProperty +{ + protected byte mValue; - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, 1, new byte[] { mValue }); - } - } + public ExifByte(ExifTag tag, byte value) + : base(tag) => + mValue = value; + + public new byte Value + { + get => mValue; + set => mValue = value; } - /// - /// Represents an array of 8-bit unsigned integers. (EXIF Specification: BYTE with count > 1) - /// - internal class ExifByteArray : ExifProperty + protected override 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; } } + get => Value; + set => Value = Convert.ToByte(value); + } - static public implicit operator byte[](ExifByteArray obj) { return obj.mValue; } + public override ExifInterOperability Interoperability => + new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, 1, new[] { 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 static implicit operator byte(ExifByte obj) => obj.mValue; - public ExifByteArray(ExifTag tag, byte[] value) - : base(tag) - { - mValue = value; - } + 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 + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (byte[])value; + } + + 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 ExifInterOperability Interoperability + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)mValue.Length, mValue); - } + sb.Append(b); + sb.Append(' '); } + + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); } +} - /// - /// Represents an ASCII string. (EXIF Specification: ASCII) - /// - internal class ExifAscii : ExifProperty +/// +/// Represents an ASCII string. (EXIF Specification: ASCII) +/// +internal class ExifAscii : ExifProperty +{ + protected string mValue; + + public ExifAscii(ExifTag tag, string value, Encoding encoding) + : base(tag) { - protected string mValue; - protected override object _Value { get { return Value; } set { Value = (string)value; } } - public new string Value { get { return mValue; } set { mValue = value; } } + mValue = value; + Encoding = encoding; + } - public Encoding Encoding { get; private set; } + public new string Value + { + get => mValue; + set => mValue = value; + } - static public implicit operator string(ExifAscii obj) { return obj.mValue; } + protected override object _Value + { + get => Value; + set => Value = (string)value; + } - public override string ToString() { return mValue; } + public Encoding Encoding { get; } - public ExifAscii(ExifTag tag, string value, Encoding encoding) - : base(tag) - { - mValue = value; - Encoding = encoding; - } + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 2, + (uint)mValue.Length + 1, + ExifBitConverter.GetBytes(mValue, true, Encoding)); - public override ExifInterOperability Interoperability - { - get - { - return 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; } - /// - /// 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; } } + get => Value; + set => Value = Convert.ToUInt16(value); + } - static public implicit operator ushort(ExifUShort obj) { return obj.mValue; } + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 3, + 1, + BitConverterEx.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); - public override string ToString() { return mValue.ToString(); } + public static implicit operator ushort(ExifUShort obj) => obj.mValue; - public ExifUShort(ExifTag tag, ushort value) - : base(tag) - { - mValue = value; - } + public override string ToString() => mValue.ToString(); +} - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 3, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); - } - } +/// +/// 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; } - /// - /// Represents an array of 16-bit unsigned integers. - /// (EXIF Specification: SHORT with count > 1) - /// - internal class ExifUShortArray : ExifProperty + protected override object _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; } } + get => Value; + set => Value = (ushort[])value; + } - static public implicit operator ushort[](ExifUShortArray obj) { return obj.mValue; } + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 3, + (uint)mValue.Length, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - 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 static implicit operator ushort[](ExifUShortArray obj) => obj.mValue; - public ExifUShortArray(ExifTag tag, ushort[] value) - : base(tag) + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) { - mValue = value; + sb.Append(b); + sb.Append(' '); } - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 3, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); } +} - /// - /// Represents a 32-bit unsigned integer. (EXIF Specification: LONG) - /// - internal class ExifUInt : ExifProperty +/// +/// 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 { - 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; } } + get => mValue; + set => mValue = value; + } - static public implicit operator uint(ExifUInt obj) { return obj.mValue; } + protected override object _Value + { + get => Value; + set => Value = Convert.ToUInt32(value); + } - public override string ToString() { return mValue.ToString(); } + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 4, + 1, + BitConverterEx.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); - public ExifUInt(ExifTag tag, uint value) - : base(tag) - { - mValue = value; - } + public static implicit operator uint(ExifUInt obj) => obj.mValue; - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 4, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); - } - } + 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; } - /// - /// Represents an array of 16-bit unsigned integers. - /// (EXIF Specification: LONG with count > 1) - /// - internal class ExifUIntArray : ExifProperty + protected override object _Value { - protected uint[] mValue; - protected override object _Value { get { return Value; } set { Value = (uint[])value; } } - public new uint[] Value { get { return mValue; } set { mValue = value; } } + get => Value; + set => Value = (uint[])value; + } - static public implicit operator uint[](ExifUIntArray obj) { return obj.mValue; } + public override ExifInterOperability Interoperability => new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 3, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - public override string ToString() - { - 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(); - } + public static implicit operator uint[](ExifUIntArray obj) => obj.mValue; - public ExifUIntArray(ExifTag tag, uint[] value) - : base(tag) + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) { - mValue = value; + sb.Append(b); + sb.Append(' '); } - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 3, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } + 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 +/// +/// 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 { - 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; } } + get => mValue; + set => mValue = value; + } - public override string ToString() { return mValue.ToString(); } - public float ToFloat() { return (float)mValue; } + protected override object _Value + { + get => Value; + set => Value = (MathEx.UFraction32)value; + } - static public explicit operator float(ExifURational obj) { return (float)obj.mValue; } + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 5, + 1, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - public uint[] ToArray() - { - return new uint[] { mValue.Numerator, mValue.Denominator }; - } + public static explicit operator float(ExifURational obj) => (float)obj.mValue; - public ExifURational(ExifTag tag, uint numerator, uint denominator) - : base(tag) - { - mValue = new MathEx.UFraction32(numerator, denominator); - } + public override string ToString() => mValue.ToString(); - public ExifURational(ExifTag tag, MathEx.UFraction32 value) - : base(tag) - { - mValue = value; - } + public float ToFloat() => (float)mValue; - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 5, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } + 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; } - /// - /// Represents an array of unsigned rational numbers. - /// (EXIF Specification: RATIONAL with count > 1) - /// - internal class ExifURationalArray : ExifProperty + protected override object _Value { - 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; } } + get => Value; + set => Value = (MathEx.UFraction32[])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 ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 5, + (uint)mValue.Length, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - public override string ToString() + public static explicit operator float[](ExifURationalArray obj) + { + var result = new float[obj.mValue.Length]; + for (var i = 0; i < obj.mValue.Length; i++) { - 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(); + result[i] = (float)obj.mValue[i]; } - public ExifURationalArray(ExifTag tag, MathEx.UFraction32[] value) - : base(tag) - { - mValue = value; - } + return result; + } - public override ExifInterOperability Interoperability + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (MathEx.UFraction32 b in mValue) { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 5, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } + 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 +/// +/// 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 { - protected byte[] mValue; - protected override object _Value { get { return Value; } set { Value = (byte[])value; } } - public new byte[] Value { get { return mValue; } set { mValue = value; } } + get => Value; + set => Value = (byte[])value; + } - static public implicit operator byte[](ExifUndefined obj) { return obj.mValue; } + public override ExifInterOperability Interoperability => + new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, (uint)mValue.Length, 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 static implicit operator byte[](ExifUndefined obj) => obj.mValue; - public ExifUndefined(ExifTag tag, byte[] value) - : base(tag) + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) { - mValue = value; + sb.Append(b); + sb.Append(' '); } - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, (uint)mValue.Length, mValue); - } - } + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); } +} - /// - /// Represents a 32-bit signed integer. (EXIF Specification: SLONG) - /// - internal class ExifSInt : ExifProperty +/// +/// 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 { - 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; } } + get => mValue; + set => mValue = value; + } - public override string ToString() { return mValue.ToString(); } + protected override object _Value + { + get => Value; + set => Value = Convert.ToInt32(value); + } - static public implicit operator int(ExifSInt obj) { return obj.mValue; } + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 9, + 1, + BitConverterEx.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); - public ExifSInt(ExifTag tag, int value) - : base(tag) - { - mValue = value; - } + public static implicit operator int(ExifSInt obj) => obj.mValue; - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 9, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); - } - } + 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; } - /// - /// Represents an array of 32-bit signed integers. - /// (EXIF Specification: SLONG with count > 1) - /// - internal class ExifSIntArray : ExifProperty + protected override object _Value { - protected int[] mValue; - protected override object _Value { get { return Value; } set { Value = (int[])value; } } - public new int[] Value { get { return mValue; } set { mValue = value; } } + get => Value; + set => Value = (int[])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(); - } + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 9, + (uint)mValue.Length, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - static public implicit operator int[](ExifSIntArray obj) { return obj.mValue; } + public static implicit operator int[](ExifSIntArray obj) => obj.mValue; - public ExifSIntArray(ExifTag tag, int[] value) - : base(tag) + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) { - mValue = value; + sb.Append(b); + sb.Append(' '); } - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 9, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } + 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 +/// +/// 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 { - 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; } } + get => mValue; + set => mValue = value; + } - public override string ToString() { return mValue.ToString(); } - public float ToFloat() { return (float)mValue; } + protected override object _Value + { + get => Value; + set => Value = (MathEx.Fraction32)value; + } - static public explicit operator float(ExifSRational obj) { return (float)obj.mValue; } + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 10, + 1, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - public int[] ToArray() - { - return new int[] { mValue.Numerator, mValue.Denominator }; - } + public static explicit operator float(ExifSRational obj) => (float)obj.mValue; - public ExifSRational(ExifTag tag, int numerator, int denominator) - : base(tag) - { - mValue = new MathEx.Fraction32(numerator, denominator); - } + public override string ToString() => mValue.ToString(); - public ExifSRational(ExifTag tag, MathEx.Fraction32 value) - : base(tag) - { - mValue = value; - } + public float ToFloat() => (float)mValue; - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 10, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } + 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; } - /// - /// Represents an array of signed rational numbers. - /// (EXIF Specification: SRATIONAL with count > 1) - /// - internal class ExifSRationalArray : ExifProperty + protected override object _Value { - 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; } } + get => Value; + set => Value = (MathEx.Fraction32[])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 ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 10, + (uint)mValue.Length, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - public override string ToString() + public static explicit operator float[](ExifSRationalArray obj) + { + var result = new float[obj.mValue.Length]; + for (var i = 0; i < obj.mValue.Length; i++) { - 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(); + result[i] = (float)obj.mValue[i]; } - public ExifSRationalArray(ExifTag tag, MathEx.Fraction32[] value) - : base(tag) - { - mValue = value; - } + return result; + } - public override ExifInterOperability Interoperability + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (MathEx.Fraction32 b in mValue) { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 10, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } + 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 7114b2eb1416..f77f0c89cd10 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 { + #region Constructor + + internal ExifPropertyCollection(ImageFile parentFile) + { + parent = parentFile; + items = new Dictionary(); + } + + #endregion + + #region Member Variables + + private readonly ImageFile parent; + private readonly Dictionary items; + + #endregion + + #region Properties + /// - /// Represents a collection of objects. + /// Gets the number of elements contained in the collection. /// - internal class ExifPropertyCollection : IDictionary - { - #region Member Variables - private ImageFile parent; - private Dictionary items; - #endregion + public int Count => items.Count; - #region Constructor - internal ExifPropertyCollection (ImageFile parentFile) + /// + /// 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 { - 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); + 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) + } + + #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)) { - 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)); - } + items.Remove(key); } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, ushort value) + + 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)) { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifUShort (key, value)); + items.Remove(key); } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, int value) + + if (key == ExifTag.WindowsTitle || key == ExifTag.WindowsComment || key == ExifTag.WindowsAuthor || + key == ExifTag.WindowsKeywords || key == ExifTag.WindowsSubject) { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifSInt (key, value)); + items.Add(key, new WindowsByteString(key, value)); } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, uint value) + else { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifUInt (key, value)); + 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, float value) + } + + /// + /// 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)) { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifURational (key, new MathEx.UFraction32 (value))); + items.Remove(key); } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, double value) + + 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)) { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifURational (key, new MathEx.UFraction32 (value))); + items.Remove(key); } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, object value) + + 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)) { - 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"); + items.Remove(key); } - /// - /// 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) + + 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)) { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifEncodedString (key, value, encoding)); + items.Remove(key); } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, DateTime value) + + 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)) { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifDateTime (key, value)); + items.Remove(key); } - /// - /// 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) + + 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) { - 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) })); + 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); + } } - #endregion - - #region Instance Methods - /// - /// Adds the specified item to the collection. - /// - /// The to add to the collection. - public void Add (ExifProperty item) + else { - ExifProperty? oldItem = null; - if (items.TryGetValue (item.Tag, out oldItem)) - items[item.Tag] = item; - else - items.Add (item.Tag, item); + throw new ArgumentException("No exif property exists for this tag.", "value"); } - /// - /// Removes all items from the collection. - /// - public void Clear () + } + + /// + /// 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.Clear (); + items.Remove(key); } - /// - /// 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.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)) { - return items.ContainsKey (key); + items.Remove(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.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)) { - return items.Remove (key); + items.Remove(key); } - /// - /// Removes all items with the given IFD from the collection. - /// - /// The IFD section to remove. - public void Remove (IFD ifd) + + 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)) { - 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); + items[item.Tag] = item; } - /// - /// 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) + else { - return items.TryGetValue (key, out value); + items.Add(item.Tag, item); } - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// - public IEnumerator GetEnumerator () + } + + /// + /// 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) { - return Values.GetEnumerator (); + if (item.Value.IFD == ifd) + { + toRemove.Add(item.Key); + } } - #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) + + foreach (ExifTag tag in toRemove) { - Add (value); + items.Remove(tag); } - /// - /// Adds an item to the . - /// - /// The object to add to the . - /// The is read-only. - void ICollection>.Add (KeyValuePair item) + } + + /// + /// 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) { - Add (item.Value); + throw new ArgumentNullException("array"); } - bool ICollection>.Contains (KeyValuePair item) + + if (arrayIndex < 0) { - throw new NotSupportedException (); + throw new ArgumentOutOfRangeException("arrayIndex"); } - /// - /// 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.Rank > 1) { - 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; } + throw new ArgumentException("Destination array is multidimensional.", "array"); } - /// - /// 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) + + if (arrayIndex >= array.Length) { - throw new NotSupportedException (); + throw new ArgumentException("arrayIndex is equal to or greater than the length of destination array", "array"); } - /// - /// 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 () + + if (arrayIndex + items.Count > array.Length) { - return GetEnumerator (); + throw new ArgumentException("There is not enough space in destination array.", "array"); } - /// - /// Returns an enumerator that iterates through the collection. - /// - /// - /// A that can be used to iterate through the collection. - /// - IEnumerator> IEnumerable>.GetEnumerator () + + var i = 0; + foreach (KeyValuePair item in items) { - return items.GetEnumerator (); + if (i >= arrayIndex) + { + array[i] = item; + } + + i++; } - #endregion } + + /// + /// 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 f47cab1c35bd..4290dcaf7cd8 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 +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Creates exif properties from interoperability parameters. +/// +internal static class ExifPropertyFactory { + #region Static Methods + /// - /// Creates exif properties from interoperability parameters. + /// Creates an ExifProperty from the given interoperability parameters. /// - internal static class ExifPropertyFactory + /// 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) { - #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) { - BitConverterEx 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) - { - 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)); - } - else if (ifd == IFD.EXIF) - { - 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 + // Compression + if (tag == 0x103) + { + return new ExifEnumProperty(ExifTag.Compression, (Compression)conv.ToUInt16(value, 0)); + } + + // PhotometricInterpretation + if (tag == 0x106) + { + 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); + 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)); + } - return new ExifEncodedString(ExifTag.UserComment, val, enc); + // 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) + { + return new ExifCircularSubjectArea( + ExifTag.SubjectArea, + ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); } - 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 + + if (count == 4) { - 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 ExifRectangularSubjectArea( + 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 (type == 1) // 1 = BYTE An 8-bit unsigned integer. - { - if (count == 1) - return new ExifByte(etag, value[0]); - else - return new ExifByteArray(etag, value); + + return new ExifPointSubjectArea( + ExifTag.SubjectArea, + ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); } - else if (type == 2) // 2 = ASCII An 8-bit byte containing one 7-bit ASCII code. + + // FocalPlaneResolutionUnit + if (tag == 0xa210) { - return new ExifAscii(etag, ExifBitConverter.ToAscii(value, encoding), encoding); + return new ExifEnumProperty( + ExifTag.FocalPlaneResolutionUnit, + (ResolutionUnit)conv.ToUInt16(value, 0), + true); } - else if (type == 3) // 3 = SHORT A 16-bit (2-byte) unsigned integer. + + // SubjectLocation + if (tag == 0xa214) { - if (count == 1) - return new ExifUShort(etag, conv.ToUInt16(value, 0)); - else - return new ExifUShortArray(etag, ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); + return new ExifPointSubjectArea( + ExifTag.SubjectLocation, + ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); } - else if (type == 4) // 4 = LONG A 32-bit (4-byte) unsigned integer. + + // SensingMethod + if (tag == 0xa217) { - 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.SensingMethod, + (SensingMethod)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. + + // FileSource + if (tag == 0xa300) { - 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.FileSource, (FileSource)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). + + // SceneType + if (tag == 0xa301) { - 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.SceneType, (SceneType)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. + + // CustomRendered + if (tag == 0xa401) { - 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.CustomRendered, + (CustomRendered)conv.ToUInt16(value, 0), + true); + } + + // ExposureMode + if (tag == 0xa402) + { + 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 22215044b2f9..0ffd754836aa 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 6a5ea8494424..726da925aa24 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); - } + IFD ifd = GetTagIFD(exiftag); + return (ushort)((int)exiftag - (int)ifd); + } - /// - /// 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) => (IFD)((int)tag / 100000 * 100000); - /// - /// Returns the IFD section containing the given tag. - /// - public static IFD GetTagIFD(ExifTag tag) + /// + /// 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 (IFD)(((int)tag / 100000) * 100000); + return "Unknown"; } - /// - /// 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; - } + 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 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) + /// + /// 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) { - 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 + ")"; + name = "Unknown"; } - #endregion + + 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 e275e8d52a85..cda3cdcb69b7 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 cb783d3ee9fc..23ea615be9e9 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) + 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)) { - using (FileStream stream = new FileStream (filename, FileMode.Create, FileAccess.Write, FileShare.None)) { - Save (stream); - } + 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); - /// - /// 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) + /// + /// 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 FromFile(filename, Encoding.Default); + return FromStream(stream, encoding); } + } - /// - /// 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) + /// + /// 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)) { - using (FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - return FromStream(stream, encoding); - } + return new JPEGFile(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) + // TIFF + if (TIFFDetector.IsOfType(stream)) { - return FromStream(stream, Encoding.Default); + return new TIFFFile(stream, encoding); } - /// - /// 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) + // SVG + if (SvgDetector.IsOfType(stream)) { - // 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; + return new SvgFile(stream); } - #endregion + + // 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 ed4564a48603..299e7619f950 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() - { - Fields = new List(); - Strips = new List(); - } + /// + /// The fields contained in this IFD. + /// + public List Fields { 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) - { - ImageFileDirectory ifd = new ImageFileDirectory(); - BitConverterEx conv = new BitConverterEx(byteOrder, BitConverterEx.SystemByteOrder); + /// + /// Offset to the next IFD. + /// + public uint NextIFDOffset { get; private set; } - List stripOffsets = new List(); - List stripLengths = new List(); + /// + /// Compressed image data. + /// + public List Strips { get; } - // Count - ushort fieldcount = conv.ToUInt16(data, offset); + /// + /// 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); - // Read fields - for (uint i = 0; i < fieldcount; i++) - { - uint fieldoffset = offset + 2 + 12 * i; - ImageFileDirectoryEntry field = ImageFileDirectoryEntry.FromBytes(data, fieldoffset, byteOrder); - ifd.Fields.Add(field); + var stripOffsets = new List(); + var stripLengths = new List(); - // Read strip offsets - if (field.Tag == 273) + // Count + var fieldcount = conv.ToUInt16(data, offset); + + // Read fields + for (uint i = 0; i < fieldcount; i++) + { + var fieldoffset = offset + 2 + (12 * i); + var 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); - } + 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); } + } - // Read strip lengths - if (field.Tag == 279) + // Read strip lengths + if (field.Tag == 279) + { + 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 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 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 (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); + // Save strips + if (stripOffsets.Count != stripLengths.Count) + { + throw new NotValidTIFFileException(); + } - return ifd; + 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 7d1568afb321..a3863b6a6990 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; - - /// - /// 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) + Tag = tag; + Type = type; + Count = count; + Data = 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); + for (uint i = 0; i < count; i++) + { + var val = new byte[baselength]; + Array.Copy(value, i * baselength, val, 0, baselength); + Array.Reverse(val); + Array.Copy(val, 0, value, i * baselength, baselength); + } + } - // Tag Type - ushort type = BitConverterEx.ToUInt16(data, offset + 2, byteOrder, BitConverterEx.SystemByteOrder); + return new ImageFileDirectoryEntry(tag, type, count, value); + } - // Count of Type - uint count = BitConverterEx.ToUInt32(data, offset + 4, byteOrder, BitConverterEx.SystemByteOrder); + /// + /// 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) + { + return 1; + } - // Field value or offset to field data - byte[] value = new byte[4]; - Array.Copy(data, offset + 8, value, 0, 4); + // ASCII and UNDEFINED + if (type == 2 || type == 7) + { + return 1; + } - // Calculate the bytes we need to read - uint baselength = GetBaseLength(type); - uint totallength = count * baselength; + // SHORT and SSHORT + if (type == 3 || type == 8) + { + return 2; + } - // If field value does not fit in 4 bytes - // the value field is an offset to the actual - // field value - if (totallength > 4) - { - uint dataoffset = BitConverterEx.ToUInt32(value, 0, byteOrder, BitConverterEx.SystemByteOrder); - value = new byte[totallength]; - Array.Copy(data, dataoffset, value, 0, totallength); - } + // LONG and SLONG + if (type == 4 || type == 9) + { + return 4; + } - // 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); - } - } + // RATIONAL (2xLONG) and SRATIONAL (2xSLONG) + if (type == 5 || type == 10) + { + return 8; + } - return new ImageFileDirectoryEntry(tag, type, count, value); + // FLOAT + if (type == 11) + { + return 4; } - /// - /// Gets the base byte length for the given type. - /// - /// Type identifier. - private static uint GetBaseLength(ushort type) + // DOUBLE + if (type == 12) { - 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 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 09cfcce589cd..fe30c713b255 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 ff6b0463ed3b..438d7bf3d4d6 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 d3a0e7fb46d7..71ea89228d90 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) - { + } - } + /// + /// Gets the major version. + /// + public byte Major => (byte)(mValue >> 8); - public override string ToString() - { - return string.Format("{0}.{1:00}", Major, Minor); - } - } /// - /// Represents a JFIF thumbnail. (EXIF Specification: BYTE) + /// Gets the minor version. /// - internal class JFIFThumbnailProperty : ExifProperty + public byte Minor => (byte)(mValue - ((mValue >> 8) * 256)); + + 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 { - protected JFIFThumbnail mValue; - protected override object _Value { get { return Value; } set { Value = (JFIFThumbnail)value; } } - public new JFIFThumbnail Value { get { return mValue; } set { mValue = value; } } + get => mValue; + set => mValue = value; + } - public override string ToString() { return mValue.Format.ToString(); } + protected override object _Value + { + get => Value; + set => Value = (JFIFThumbnail)value; + } - public JFIFThumbnailProperty(ExifTag tag, JFIFThumbnail value) - : base(tag) + public override ExifInterOperability Interoperability + { + get { - mValue = value; - } + if (mValue.Format == JFIFThumbnail.ImageFormat.BMP24Bit) + { + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)mValue.PixelData.Length, mValue.PixelData); + } - public override ExifInterOperability Interoperability - { - get + if (mValue.Format == JFIFThumbnail.ImageFormat.BMPPalette) { - 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."); + 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 de9fe8f76fb7..cafa804c3a4b 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 { + #region Public Enums + + public enum ImageFormat + { + JPEG, + BMPPalette, + BMP24Bit, + } + + #endregion + + #region Properties + + /// + /// Gets the 256 color RGB palette. + /// + public byte[] Palette { get; } + /// - /// Represents a JFIF thumbnail. + /// Gets raw image data. /// - internal class JFIFThumbnail + public byte[] PixelData { get; } + + /// + /// Gets the image format. + /// + public ImageFormat Format { get; } + + #endregion + + #region Constructors + + protected JFIFThumbnail() { - #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 + 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 dde0326f9996..c44d6d1db0db 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 +{ + /// + /// 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. +/// +/// +[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) + { + } /// - /// The exception that is thrown when the format of the JPEG file could not be understood. - /// - /// - [Serializable] - public class NotValidJPEGFileException : Exception - { - /// - /// 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. - /// - /// - [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 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) - { } - } - - /// - /// 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) - { } + /// 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) + { } } diff --git a/src/Umbraco.Core/Media/Exif/JPEGFile.cs b/src/Umbraco.Core/Media/Exif/JPEGFile.cs index f0f732b520f1..bdf7208ea06a 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 - - #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 - - #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) - { - Format = ImageFileFormat.JPEG; - Sections = new List(); - TrailingData = new byte[0]; - Encoding = encoding; - - 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) + Format = ImageFileFormat.JPEG; + Sections = new List(); + TrailingData = new byte[0]; + Encoding = encoding; + + stream.Seek(0, SeekOrigin.Begin); + + // 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) + { + throw new NotValidJPEGFileException(); + } + + stream.Seek(0, SeekOrigin.Begin); + + // 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) + { throw new NotValidJPEGFileException(); - stream.Seek(0, SeekOrigin.Begin); + } + + var marker = (JPEGMarker)markerbytes[1]; - // Search and read sections until we reach the end of file. - while (stream.Position != stream.Length) + 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)) { - // 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) + // 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) + { throw new NotValidJPEGFileException(); + } - JPEGMarker marker = (JPEGMarker)markerbytes[1]; + long length = BitConverterEx.BigEndian.ToUInt16(lengthbytes, 0); - 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)) + // Read section header. + header = new byte[length - 2]; + var bytestoread = header.Length; + while (bytestoread > 0) { - // 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) + var count = Math.Min(bytestoread, 4 * 1024); + var bytesread = stream.Read(header, header.Length - bytestoread, count); + if (bytesread == 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(); } + + bytestoread -= bytesread; } + } - // 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 position = stream.Position; + // 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; - // Search for the next section marker - while (true) + // Search for the next section marker + while (true) + { + // Search for an 0xFF indicating start of a marker + var nextbyte = 0; + do { - // Search for an 0xFF indicating start of a marker - int nextbyte = 0; - do + nextbyte = stream.ReadByte(); + if (nextbyte == -1) { - nextbyte = stream.ReadByte(); - if (nextbyte == -1) - throw new NotValidJPEGFileException(); - } while ((byte)nextbyte != 0xFF); + throw new NotValidJPEGFileException(); + } + } + while ((byte)nextbyte != 0xFF); - // Skip filler bytes (0xFF) - do + // Skip filler bytes (0xFF) + do + { + nextbyte = stream.ReadByte(); + if (nextbyte == -1) { - nextbyte = stream.ReadByte(); - if (nextbyte == -1) - throw new NotValidJPEGFileException(); - } while ((byte)nextbyte == 0xFF); + throw new NotValidJPEGFileException(); + } + } + while ((byte)nextbyte == 0xFF); - // Looks like a section marker. The next byte must not be 0x00. - if ((byte)nextbyte != 0x00) + // 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) { - // 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); - - // Read entropy coded data - entropydata = new byte[edlength]; - int 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(); + // Read metadata sections + ReadJFIFAPP0(); + ReadJFXXAPP0(); + ReadExifAPP1(); - // Process the maker note - makerNoteProcessed = false; - } - #endregion + // 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) + #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); + // Section header (including length bytes and section marker) + // must not exceed 64 kB. + if (section.Header.Length + 2 + 2 > 64 * 1024) + { + throw new SectionExceeds64KBException(); + } - // Write sections - foreach (JPEGSection section in Sections) + // APP sections must have a header. + // Otherwise skip the entire section. + if (section.Marker >= JPEGMarker.APP0 && section.Marker <= JPEGMarker.APP15 && section.Header.Length == 0) { - // Section header (including length bytes and section marker) - // must not exceed 64 kB. - if (section.Header.Length + 2 + 2 > 64 * 1024) - throw new SectionExceeds64KBException(); + 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)) + { + // Header length including the length field itself + stream.Write(BitConverterEx.BigEndian.GetBytes((ushort)(section.Header.Length + 2)), 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)) + // 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 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 (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 - // Write trailing data, if any - if (TrailingData.Length != 0) - stream.Write(TrailingData, 0, TrailingData.Length); + /// + /// 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 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) + 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) { - using (FileStream stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None)) + if (prop.IFD == IFD.JFIF) { - Save(stream, preserveMakerNote); + ifdjfef.Add(prop); } } - /// - /// Saves the JPEG/Exif image with the given filename. - /// - /// The path to the JPEG/Exif file. - public override void Save(string filename) + if (ifdjfef.Count == 0) { - Save(filename, true); + // Nothing to write + return false; } - /// - /// Saves the JPEG/Exif image to the given stream. - /// - /// The path to the JPEG/Exif file. - public override void Save(Stream stream) + // Create a memory stream to write the APP0 section to + var ms = new MemoryStream(); + + // JFIF identifier + ms.Write(Encoding.ASCII.GetBytes("JFIF\0"), 0, 5); + + // Write tags + foreach (ExifProperty prop in ifdjfef) { - Save(stream, true); + ExifInterOperability interop = prop.Interoperability; + var data = interop.Data; + if (BitConverterEx.SystemByteOrder != BitConverterEx.ByteOrder.BigEndian && interop.TypeID == 3) + { + Array.Reverse(data); + } + + ms.Write(data, 0, data.Length); } - #endregion + ms.Close(); - #region Private Helper Methods - /// - /// Reads the APP0 section containing JFIF metadata. - /// - private void ReadJFIFAPP0() + // Return APP0 header + if (_jfifApp0 is not null) { - // 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")); + _jfifApp0.Header = ms.ToArray(); + return true; + } - // If there is no APP0 section, return. - if (jfifApp0 == null) - return; + return false; + } - byte[] header = jfifApp0.Header; - BitConverterEx jfifConv = BitConverterEx.BigEndian; + /// + /// 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"); - // Version - ushort version = jfifConv.ToUInt16(header, 5); - Properties.Add(new JFIFVersion(ExifTag.JFIFVersion, version)); + // If there is no APP0 section, return. + if (_jfxxApp0 == null) + { + return; + } - // Units - byte unit = header[7]; - Properties.Add(new ExifEnumProperty(ExifTag.JFIFUnits, (JFIFDensityUnit)unit)); + var header = _jfxxApp0.Header; - // 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)); + // 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) - { - if (prop.IFD == IFD.JFIF) - ifdjfef.Add(prop); - } + // 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))); + } + } - if (ifdjfef.Count == 0) + /// + /// 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) { - // Nothing to write - return false; + ifdjfef.Add(prop); } + } + if (ifdjfef.Count == 0) + { + // 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("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); - } + // Create a memory stream to write the APP0 section to + var ms = new MemoryStream(); - ms.Close(); + // JFIF identifier + ms.Write(Encoding.ASCII.GetBytes("JFXX\0"), 0, 5); - // Return APP0 header - if (jfifApp0 is not null) + // Write tags + foreach (ExifProperty prop in ifdjfef) + { + ExifInterOperability interop = prop.Interoperability; + var data = interop.Data; + if (BitConverterEx.SystemByteOrder != BitConverterEx.ByteOrder.BigEndian && interop.TypeID == 3) { - jfifApp0.Header = ms.ToArray(); - return true; + Array.Reverse(data); } - return false; + ms.Write(data, 0, data.Length); } - /// - /// 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")); + ms.Close(); - // If there is no APP0 section, return. - if (jfxxApp0 == null) - return; + if (_jfxxApp0 is not null) + { + // Return APP0 header + _jfxxApp0.Header = ms.ToArray(); + return true; + } - byte[] header = jfxxApp0.Header; + return false; + } - // Version - JFIFExtension version = (JFIFExtension)header[5]; - Properties.Add(new ExifEnumProperty(ExifTag.JFXXExtensionCode, version)); + /// + /// 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"); - // Read thumbnail - if (version == JFIFExtension.ThumbnailJPEG) + // If there is no APP1 section, add a new one after the last APP0 section (if any). + if (_exifApp1 == null) + { + 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))); + insertionIndex = 0; } - else if (version == JFIFExtension.Thumbnail24BitRGB) + + insertionIndex++; + _exifApp1 = new JPEGSection(JPEGMarker.APP1); + Sections.Insert(insertionIndex, _exifApp1); + if (BitConverterEx.SystemByteOrder == BitConverterEx.ByteOrder.LittleEndian) { - // 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))); + ByteOrder = BitConverterEx.ByteOrder.LittleEndian; } - else if (version == JFIFExtension.ThumbnailPaletteRGB) + else { - // 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))); + ByteOrder = BitConverterEx.ByteOrder.BigEndian; } + + return; } - /// - /// Replaces the contents of the APP0 section with the JFIF extension properties. - /// - private bool WriteJFXXApp0() + + var header = _exifApp1.Header; + var ifdqueue = new SortedList(); + _makerNoteOffset = 0; + + // TIFF header + var tiffoffset = 6; + if (header[tiffoffset] == 0x49 && header[tiffoffset + 1] == 0x49) { - // Which IFD sections do we have? - List ifdjfef = new List(); - foreach (ExifProperty prop in Properties) - { - if (prop.IFD == IFD.JFXX) - ifdjfef.Add(prop); - } + ByteOrder = BitConverterEx.ByteOrder.LittleEndian; + } + else if (header[tiffoffset] == 0x4D && header[tiffoffset + 1] == 0x4D) + { + ByteOrder = BitConverterEx.ByteOrder.BigEndian; + } + else + { + throw new NotValidExifFileException(); + } - if (ifdjfef.Count == 0) - { - // Nothing to write - return false; - } + // 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 + var ifd0offset = (int)BitConverterEx.ToUInt32(header, tiffoffset + 4, tiffByteOrder, BitConverterEx.SystemByteOrder); + ifdqueue.Add(ifd0offset, IFD.Zeroth); - // Create a memory stream to write the APP0 section to - MemoryStream ms = new MemoryStream(); + var conv = new BitConverterEx(ByteOrder, BitConverterEx.SystemByteOrder); + var thumboffset = -1; + var thumblength = 0; + var thumbtype = -1; - // JFIF identifier - ms.Write(Encoding.ASCII.GetBytes("JFXX\0"), 0, 5); + // Read IFDs + while (ifdqueue.Count != 0) + { + var ifdoffset = tiffoffset + ifdqueue.Keys[0]; + IFD currentifd = ifdqueue.Values[0]; + ifdqueue.RemoveAt(0); - // Write tags - foreach (ExifProperty prop in ifdjfef) + // Field count + var fieldcount = conv.ToUInt16(header, ifdoffset); + for (short i = 0; i < fieldcount; i++) { - 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); - } + // 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); - ms.Close(); + // Fields containing offsets to other IFDs + 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); + } - if (jfxxApp0 is not null) - { - // Return APP0 header - jfxxApp0.Header = ms.ToArray(); - return true; - } + // Save the offset to maker note data + if (currentifd == IFD.EXIF && tag == 37500) + { + _makerNoteOffset = conv.ToUInt32(value, 0); + } - return false; - } + // 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; + } - /// - /// 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")); + var totallength = count * baselength; - // 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; - } + // 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); + } - byte[] header = exifApp1.Header; - SortedList ifdqueue = new SortedList(); - makerNoteOffset = 0; + // 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); + } - // 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) - 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++) + // Uncompressed thumbnail data + if (currentifd == IFD.First && tag == 0x111) { - // 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); - } + thumbtype = 1; - // 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) + // Offset to first strip + if (type == 3) { - fieldposition = tiffoffset + (int)conv.ToUInt32(value, 0); - value = new byte[totallength]; - Array.Copy(header, fieldposition, value, 0, totallength); + thumboffset = conv.ToUInt16(value, 0); } - - // Compressed thumbnail data - if (currentifd == IFD.First && tag == 0x201) + else { - 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) + } + else if (currentifd == IFD.First && tag == 0x117) + { + thumblength = 0; + for (var j = 0; j < count; j++) { - thumbtype = 1; - // Offset to first strip if (type == 3) - thumboffset = (int)conv.ToUInt16(value, 0); + { + thumblength += 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); + 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) + // 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 + var 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) { - if (thumbtype == 0) + using (var ts = new MemoryStream(header, tiffoffset + thumboffset, thumblength)) { - using (MemoryStream ts = new MemoryStream(header, tiffoffset + thumboffset, thumblength)) - { - Thumbnail = ImageFile.FromStream(ts); - } + Thumbnail = FromStream(ts); } } } } + } - /// - /// Replaces the contents of the APP1 section with the Exif properties. - /// - private bool WriteExifApp1(bool preserveMakerNote) + /// + /// 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) { - // 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.Remove(ExifTag.ThumbnailJPEGInterchangeFormat); - Properties.Remove(ExifTag.ThumbnailJPEGInterchangeFormatLength); + Properties.Add(new ExifUInt(ExifTag.ThumbnailJPEGInterchangeFormat, 0)); } - else + + if (!Properties.ContainsKey(ExifTag.ThumbnailJPEGInterchangeFormatLength)) { - if (!Properties.ContainsKey(ExifTag.ThumbnailJPEGInterchangeFormat)) - Properties.Add(new ExifUInt(ExifTag.ThumbnailJPEGInterchangeFormat, 0)); - if (!Properties.ContainsKey(ExifTag.ThumbnailJPEGInterchangeFormatLength)) - Properties.Add(new ExifUInt(ExifTag.ThumbnailJPEGInterchangeFormatLength, 0)); + 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(); + // 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) + foreach (ExifProperty prop in Properties) + { + switch (prop.IFD) { - 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; - } + 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)); + // 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)); + } - // 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 (ifdgps.Count != 0 && !ifdzeroth.ContainsKey(ExifTag.GPSIFDPointer)) + { + ifdzeroth.Add(ExifTag.GPSIFDPointer, new ExifUInt(ExifTag.GPSIFDPointer, 0)); + } - if (ifdzeroth.Count == 0 && ifdgps.Count == 0 && ifdinterop.Count == 0 && ifdfirst.Count == 0 && Thumbnail == null) - { - // Nothing to write - return false; - } + if (ifdinterop.Count != 0 && !ifdexif.ContainsKey(ExifTag.InteroperabilityIFDPointer)) + { + ifdexif.Add(ExifTag.InteroperabilityIFDPointer, new ExifUInt(ExifTag.InteroperabilityIFDPointer, 0)); + } - // We will need these BitConverters to write byte-ordered data - BitConverterEx bceExif = new BitConverterEx(BitConverterEx.SystemByteOrder, ByteOrder); + // Remove IFD pointers if IFD sections are missing + if (ifdexif.Count == 0 && ifdzeroth.ContainsKey(ExifTag.EXIFIFDPointer)) + { + ifdzeroth.Remove(ExifTag.EXIFIFDPointer); + } - // Create a memory stream to write the APP1 section to - MemoryStream ms = new MemoryStream(); + if (ifdgps.Count == 0 && ifdzeroth.ContainsKey(ExifTag.GPSIFDPointer)) + { + ifdzeroth.Remove(ExifTag.GPSIFDPointer); + } - // Exif identifier - ms.Write(Encoding.ASCII.GetBytes("Exif\0\0"), 0, 6); + if (ifdinterop.Count == 0 && ifdexif.ContainsKey(ExifTag.InteroperabilityIFDPointer)) + { + ifdexif.Remove(ExifTag.InteroperabilityIFDPointer); + } - // 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); + if (ifdzeroth.Count == 0 && ifdgps.Count == 0 && ifdinterop.Count == 0 && ifdfirst.Count == 0 && + Thumbnail == null) + { + // Nothing to write + return false; + } - // 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); + // We will need these BitConverters to write byte-ordered data + var bceExif = new BitConverterEx(BitConverterEx.SystemByteOrder, ByteOrder); - // 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); - } + // 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); + } - ms.Close(); + // 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) + 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) { - // Return APP1 header - exifApp1.Header = ms.ToArray(); - return true; + fieldqueue.Enqueue(prop); } - - return false; } - private void WriteIFD(MemoryStream stream, Dictionary ifd, IFD ifdtype, long tiffoffset, bool preserveMakerNote) + // Push the maker note data to the end + if (ifd.ContainsKey(ExifTag.MakerNote)) { - BitConverterEx conv = new BitConverterEx(BitConverterEx.SystemByteOrder, ByteOrder); + fieldqueue.Enqueue(ifd[ExifTag.MakerNote]); + } - // 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 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); - // 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 makernotewritten = false; - bool 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)) + // 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) { - // Delay writing this field until we write the creator's note data - fieldqueue.Enqueue(field); - continue; + fillerbytecount = _makerNoteOffset - currentdataoffset; } - else if (field.Tag == ExifTag.MakerNote) + else { - 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; + 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; + // Tag + stream.Write(conv.GetBytes(interop.TagID), 0, 2); - for (int i = 0; i < n; i++) - Array.Reverse(data, i * vlen, vlen); - } + // Type + stream.Write(conv.GetBytes(interop.TypeID), 0, 2); - // 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) + // 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) { - stream.Write(data, 0, data.Length); - for (int i = data.Length; i < 4; i++) - stream.WriteByte(0); + vlen = 2; } - else + + var n = data.Length / vlen; + + for (var i = 0; i < n; i++) { - // 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; + Array.Reverse(data, i * vlen, vlen); } } - // 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); + // 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 thumbnail data - if (ifdtype == IFD.First) + // Write 4 byte field value or field data + if (data.Length <= 4) { - if (Thumbnail != null) + stream.Write(data, 0, data.Length); + for (var i = data.Length; i < 4; i++) { - 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(); + stream.WriteByte(0); } - else + } + 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++) { - thumbOffsetValue = 0; - thumbSizeValue = 0; + 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 } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/JPEGMarker.cs b/src/Umbraco.Core/Media/Exif/JPEGMarker.cs index a7a3b4a9b192..3912d87e822d 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 07dd48838447..787b04b0563a 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 + + /// + /// Returns a string representation of the current section. + /// + /// 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 + /// - /// Represents the memory view of a JPEG section. - /// A JPEG section is the data between markers of the JPEG file. + /// Constructs a JPEGSection represented by the marker byte and containing + /// the given data. /// - internal class JPEGSection + /// 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 d49ccf924f31..fbf5f2dbde5f 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; } + return a; + } + + /// + /// 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 + + #region Member Variables + + private int mNumerator; + private int mDenominator; + + #endregion + + #region Properties + /// - /// Represents a generic rational number represented by 32-bit signed numerator and denominator. + /// Gets or sets the numerator. /// - public struct Fraction32 : IComparable, IFormattable, IComparable, IEquatable + public int Numerator { - #region Constants - private const uint MaximumIterations = 10000000; - #endregion - - #region Member Variables - private bool mIsNegative; - private int mNumerator; - private int mDenominator; - private double mError; - #endregion - - #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 => (IsNegative ? -1 : 1) * mNumerator; + set { - get + if (value < 0) { - return ((int)mDenominator); + IsNegative = true; + mNumerator = -1 * value; } - set + else { - mDenominator = System.Math.Abs(value); - Reduce(ref mNumerator, ref mDenominator); + IsNegative = false; + mNumerator = value; } - } - /// - /// Gets the error term. - /// - public double Error - { - get - { - return mError; - } + Reduce(ref mNumerator, ref mDenominator); } + } - /// - /// Gets or sets a value determining id the fraction is a negative value. - /// - public bool IsNegative - { - get - { - return mIsNegative; - } - set - { - mIsNegative = value; - } - } - #endregion - - #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 - - #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) + /// + /// Gets or sets the denominator. + /// + public int Denominator + { + get => mDenominator; + set { - return new Fraction32(f.Denominator, f.Numerator); + mDenominator = Math.Abs(value); + Reduce(ref mNumerator, ref mDenominator); } + } - /// - /// 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); - } + /// + /// Gets the error term. + /// + public double Error { get; } - /// - /// 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 + /// + /// Gets or sets a value determining id the fraction is a negative value. + /// + public bool IsNegative { get; set; } - #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) + #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 { - return f * n; + f = Parse(s); + return true; } - public static Fraction32 operator *(Fraction32 f, double n) + catch { - return new Fraction32(((double)f) * n); + f = new Fraction32(); + return false; } - public static Fraction32 operator *(double n, Fraction32 f) + } + + #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) { - return f * n; + numerator = -numerator; + IsNegative = !IsNegative; } - public static Fraction32 operator *(Fraction32 f1, Fraction32 f2) + + if (denominator < 0) { - return new Fraction32(f1.Numerator * f2.Numerator, f1.Denominator * f2.Denominator); + denominator = -denominator; + IsNegative = !IsNegative; } - // Division - public static Fraction32 operator /(Fraction32 f, int n) + + mNumerator = numerator; + mDenominator = denominator; + Error = error; + + if (mDenominator != 0) { - return new Fraction32(f.Numerator / n, f.Denominator / System.Math.Abs(n)); + Reduce(ref mNumerator, ref mDenominator); } - public static Fraction32 operator /(Fraction32 f, float n) + } + + 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) { - return new Fraction32(((float)f) / n); + IsNegative = !IsNegative; + numerator = -numerator; } - public static Fraction32 operator /(Fraction32 f, double n) + + if (denominator < 0) { - return new Fraction32(((double)f) / n); + IsNegative = !IsNegative; + denominator = -denominator; } - public static Fraction32 operator /(Fraction32 f1, Fraction32 f2) + + mNumerator = numerator; + mDenominator = denominator; + + if (mDenominator != 0) { - return f1 * Inverse(f2); + Reduce(ref mNumerator, ref mDenominator); } - // Addition - public static Fraction32 operator +(Fraction32 f, int n) + } + + /// + /// 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 f + new Fraction32(n, 1); + return false; } - public static Fraction32 operator +(int n, Fraction32 f) + + if (obj is Fraction32) { - return f + n; + return Equals((Fraction32)obj); } - public static Fraction32 operator +(Fraction32 f, float n) + + 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)) { - return new Fraction32(((float)f) + n); + throw new ArgumentException("obj must be of type Fraction", "obj"); } - public static Fraction32 operator +(float n, Fraction32 f) + + 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 f + n; + return -1; } - public static Fraction32 operator +(Fraction32 f, double n) + + if (this > obj) { - return new Fraction32(((double)f) + n); + return 1; } - public static Fraction32 operator +(double n, Fraction32 f) + + 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 f + n; + return NaN; } - 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) + if (double.IsNegativeInfinity(value)) { - return f - new Fraction32(n, 1); + return NegativeInfinity; } - public static Fraction32 operator -(int n, Fraction32 f) + + if (double.IsPositiveInfinity(value)) { - return new Fraction32(n, 1) - f; + return PositiveInfinity; } - public static Fraction32 operator -(Fraction32 f, float n) + + var isneg = value < 0; + if (isneg) { - return new Fraction32(((float)f) - n); + value = -value; } - public static Fraction32 operator -(float n, Fraction32 f) + + 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) { - return new Fraction32(n) - f; + 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; } - public static Fraction32 operator -(Fraction32 f, double n) + + if (den > 0) { - return new Fraction32(((double)f) - n); + lasterr = value - (num / (double)den); } - public static Fraction32 operator -(double n, Fraction32 f) + else { - return new Fraction32(n) - f; + lasterr = double.PositiveInfinity; } - 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 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) { - return f + new Fraction32(1, 1); + throw new ArgumentNullException("s"); } - // Decrement - public static Fraction32 operator --(Fraction32 f) + + var sa = s.Split(Constants.CharArrays.ForwardSlash); + var numerator = 1; + var denominator = 1; + + if (sa.Length == 1) { - return f - new Fraction32(1, 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); + } } - #endregion - #region Casts To Integral Types - public static explicit operator int(Fraction32 f) + else if (sa.Length == 2) { - return f.Numerator / f.Denominator; + numerator = int.Parse(sa[0], CultureInfo.InvariantCulture); + denominator = int.Parse(sa[1], CultureInfo.InvariantCulture); } - public static explicit operator float(Fraction32 f) + else { - return ((float)f.Numerator) / ((float)f.Denominator); + throw new FormatException("The input string must be formatted as n/d where n and d are integers"); } - public static explicit operator double(Fraction32 f) + + 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) { - return ((double)f.Numerator) / ((double)f.Denominator); + gcd = 1; } - #endregion - #region Comparison Operators - public static bool operator ==(Fraction32 f1, Fraction32 f2) + + 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 { - return (f1.Numerator == f2.Numerator) && (f1.Denominator == f2.Denominator); + mNumerator = value; + Reduce(ref mNumerator, ref mDenominator); } - public static bool operator !=(Fraction32 f1, Fraction32 f2) + } + + /// + /// Gets or sets the denominator. + /// + public uint Denominator + { + get => mDenominator; + set { - return (f1.Numerator != f2.Numerator) || (f1.Denominator != f2.Denominator); + mDenominator = value; + Reduce(ref mNumerator, ref mDenominator); } - public static bool operator <(Fraction32 f1, Fraction32 f2) + } + + /// + /// 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 { - return (f1.Numerator * f2.Denominator) < (f2.Numerator * f1.Denominator); + f = Parse(s); + return true; } - public static bool operator >(Fraction32 f1, Fraction32 f2) + catch { - return (f1.Numerator * f2.Denominator) > (f2.Numerator * f1.Denominator); + f = new UFraction32(); + return false; } - #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; - } + #endregion - mNumerator = numerator; - mDenominator = denominator; - mError = error; + #region Operators - if (mDenominator != 0) - Reduce(ref mNumerator, ref mDenominator); - } + #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 Fraction32(int numerator, int denominator) - : this(numerator, denominator, 0) - { + 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 Fraction32(int numerator) - : this(numerator, (int)1) - { + 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; - public Fraction32(Fraction32 f) - : this(f.Numerator, f.Denominator, f.Error) - { + return new UFraction32((n1 * d2) - (n2 * d1), d1 * d2); + } - } + // Increment + public static UFraction32 operator ++(UFraction32 f) => f + new UFraction32(1, 1); - public Fraction32(float value) - : this((double)value) - { + // Decrement + public static UFraction32 operator --(UFraction32 f) => f - new UFraction32(1, 1); - } + #endregion - public Fraction32(double value) - : this(FromDouble(value)) - { + #region Casts To Integral Types - } + public static explicit operator uint(UFraction32 f) => f.Numerator / f.Denominator; - public Fraction32(string s) - : this(FromString(s)) - { + 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 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; - } + #endregion - mNumerator = numerator; - mDenominator = denominator; + #region Comparison Operators - if (mDenominator != 0) - Reduce(ref mNumerator, ref mDenominator); - } + public static bool operator ==(UFraction32 f1, UFraction32 f2) => + f1.Numerator == f2.Numerator && f1.Denominator == f2.Denominator; - /// - /// 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; + public static bool operator !=(UFraction32 f1, UFraction32 f2) => + f1.Numerator != f2.Numerator || f1.Denominator != f2.Denominator; - if (obj is Fraction32) - return Equals((Fraction32)obj); - else - return false; - } + public static bool operator <(UFraction32 f1, UFraction32 f2) => + f1.Numerator * f2.Denominator < f2.Numerator * f1.Denominator; - /// - /// 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); - } + public static bool operator >(UFraction32 f1, UFraction32 f2) => + f1.Numerator * f2.Denominator > f2.Numerator * f1.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); - } + #endregion - /// - /// 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(); - } + #endregion - /// - /// 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(); - } + #region Constructors - /// - /// 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(); - } + public UFraction32(uint numerator, uint denominator, double error) + { + mNumerator = numerator; + mDenominator = denominator; + Error = error; - /// - /// Returns a string representation of the fraction. - /// - /// A string formatted as numerator/denominator. - public override string ToString() + if (mDenominator != 0) { - StringBuilder sb = new StringBuilder(); - sb.Append(((mIsNegative ? -1 : 1) * mNumerator).ToString()); - sb.Append('/'); - sb.Append(mDenominator.ToString()); - return sb.ToString(); + Reduce(ref mNumerator, ref mDenominator); } + } - /// - /// 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"); + public UFraction32(uint numerator, uint denominator) + : this(numerator, denominator, 0) + { + } - return CompareTo((Fraction32)obj); - } + public UFraction32(uint numerator) + : this(numerator, 1) + { + } - /// - /// 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; - } + public UFraction32(UFraction32 f) + : this(f.Numerator, f.Denominator, f.Error) + { + } - if (den > 0) - lasterr = value - ((double)num / (double)den); - else - lasterr = double.PositiveInfinity; + public UFraction32(float value) + : this((double)value) + { + } - return new Fraction32((isneg ? -1 : 1) * num, den, lasterr); - } + public UFraction32(double value) + : this(FromDouble(value)) + { + } - /// 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"); + public UFraction32(string s) + : this(FromString(s)) + { + } - string[] sa = s.Split(Constants.CharArrays.ForwardSlash); - int numerator = 1; - int denominator = 1; + #endregion - 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"); + #region Instance Methods - return new Fraction32(numerator, denominator); - } + /// + /// 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; - /// - /// 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) + if (mDenominator != 0) { - uint gcd = MathEx.GCD((uint)numerator, (uint)denominator); - if (gcd == 0) gcd = 1; - numerator = numerator / (int)gcd; - denominator = denominator / (int)gcd; + Reduce(ref mNumerator, ref mDenominator); } - #endregion } /// - /// Represents a generic rational number represented by 32-bit unsigned numerator and denominator. + /// 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. /// - public struct UFraction32 : IComparable, IFormattable, IComparable, IEquatable + /// 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) { - #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 + if (obj == null) { - get - { - return mNumerator; - } - set - { - mNumerator = value; - Reduce(ref mNumerator, ref mDenominator); - } + return false; } - /// - /// Gets or sets the denominator. - /// - public uint Denominator + + if (obj is UFraction32) { - get - { - return mDenominator; - } - set - { - mDenominator = value; - Reduce(ref mNumerator, ref mDenominator); - } + return Equals((UFraction32)obj); } + return false; + } - /// - /// 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); - } + /// + /// 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; - /// - /// 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); - } + /// + /// 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; - /// - /// 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 + /// + /// 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(); + } - #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; + /// + /// 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(); + } - 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; + /// + /// 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(); + } - 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) + /// + /// 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)) { - return (f1.Numerator * f2.Denominator) > (f2.Numerator * f1.Denominator); + throw new ArgumentException("obj must be of type UFraction32", "obj"); } - #endregion - #endregion - #region Constructors - public UFraction32(uint numerator, uint denominator, double error) - { - mNumerator = numerator; - mDenominator = denominator; - mError = error; + return CompareTo((UFraction32)obj); + } - if (mDenominator != 0) - Reduce(ref mNumerator, ref mDenominator); + /// + /// 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; } - public UFraction32(uint numerator, uint denominator) - : this(numerator, denominator, 0) + if (this > obj) { - + return 1; } - public UFraction32(uint numerator) - : this(numerator, (uint)1) - { + return 0; + } - } + #endregion - public UFraction32(UFraction32 f) - : this(f.Numerator, f.Denominator, f.Error) - { + #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"); } - public UFraction32(float value) - : this((double)value) + if (double.IsNaN(value)) { - + return NaN; } - public UFraction32(double value) - : this(FromDouble(value)) + if (double.IsInfinity(value)) { - + return Infinity; } - public UFraction32(string s) - : this(FromString(s)) + 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; + } - } - #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; + a = (uint)Math.Floor(f); + f = f - a; + if (Math.Abs(f) < double.Epsilon) + { + break; + } - if (mDenominator != 0) - Reduce(ref mNumerator, ref mDenominator); - } + f = 1.0 / f; + if (double.IsInfinity(f)) + { + break; + } - /// - /// 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); - } + var cnum = (num * a) + lnum; + var cden = (den * a) + lden; + if (Math.Abs((cnum / (double)cden) - forg) < double.Epsilon) + { + break; + } - /// - /// 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; + var err = ((cnum / (double)cden) - (num / (double)den)) / (num / (double)den); - if (obj is UFraction32) - return Equals((UFraction32)obj); - else - return false; - } + // Are we converging? + if (err >= lasterr) + { + break; + } - /// - /// 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); + lasterr = err; + lnum = num; + lden = den; + num = cnum; + den = cden; } - /// - /// 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); - } + var fnum = (num * a) + lnum; + var fden = (den * a) + lden; - /// - /// 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) + if (fden > 0) { - StringBuilder sb = new StringBuilder(); - sb.Append(mNumerator.ToString(format, formatProvider)); - sb.Append('/'); - sb.Append(mDenominator.ToString(format, formatProvider)); - return sb.ToString(); + lasterr = value - (fnum / (double)fden); } - - /// - /// 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) + else { - StringBuilder sb = new StringBuilder(); - sb.Append(mNumerator.ToString(format)); - sb.Append('/'); - sb.Append(mDenominator.ToString(format)); - return sb.ToString(); + lasterr = double.PositiveInfinity; } - /// - /// 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(); - } + return new UFraction32(fnum, fden, lasterr); + } - /// - /// Returns a string representation of the fraction. - /// - /// A string formatted as numerator/denominator. - public override string ToString() + /// 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) { - StringBuilder sb = new StringBuilder(); - sb.Append(mNumerator.ToString()); - sb.Append('/'); - sb.Append(mDenominator.ToString()); - return sb.ToString(); + throw new ArgumentNullException("s"); } - /// - /// 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); - } + var sa = s.Split(Constants.CharArrays.ForwardSlash); + uint numerator = 1; + uint denominator = 1; - /// - /// 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 (sa.Length == 1) { - 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) - { - 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) + // Try to parse as uint + if (uint.TryParse(sa[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out numerator)) { - 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; + denominator = 1; } - 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); + // Parse as double + var dval = double.Parse(sa[0]); + return FromDouble(dval); } - 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) + else if (sa.Length == 2) + { + numerator = uint.Parse(sa[0], CultureInfo.InvariantCulture); + denominator = uint.Parse(sa[1], CultureInfo.InvariantCulture); + } + else { - uint gcd = MathEx.GCD(numerator, denominator); - numerator = numerator / gcd; - denominator = denominator / gcd; + throw new FormatException("The input string must be formatted as n/d where n and d are integers"); } - #endregion + + 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 b83aebe1fbe6..08326e634c59 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; - - var document = XDocument.Load(fileStream); //if it throws an exception the ugly try catch in MediaFileSystem will catch it + fileStream.Position = 0; - 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 document = + XDocument.Load(fileStream); // if it throws an exception the ugly try catch in MediaFileSystem will catch it - 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))); + 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(); - Format = ImageFileFormat.SVG; - } + 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))); - 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 8841e8337b85..2ae27c46dc44 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 - - #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) + Format = ImageFileFormat.TIFF; + IFDs = new List(); + 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) + { + throw new NotValidTIFFileException("The first IFD offset is zero."); + } + + // Read IFDs in order + while (nextIFDOffset != 0) { - Format = ImageFileFormat.TIFF; - IFDs = new List(); - Encoding = encoding; + 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 - // Read the entire stream - byte[] data = Utility.GetStreamBytes(stream); + #region Properties - // 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."); + /// + /// Gets the TIFF header. + /// + public TIFFHeader TIFFHeader { get; } + + #endregion + + #region Instance Methods - // Read IFDs in order - while (nextIFDOffset != 0) + /// + /// 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++) { - ImageFileDirectory ifd = ImageFileDirectory.FromBytes(data, nextIFDOffset, TIFFHeader.ByteOrder); - nextIFDOffset = ifd.NextIFDOffset; - IFDs.Add(ifd); + 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; } - // Process IFDs - // TODO: Add support for multiple frames - foreach (ImageFileDirectoryEntry field in IFDs[0].Fields) + // Remove old strip tags + for (var j = ifd.Fields.Count - 1; j > 0; j--) { - Properties.Add(ExifPropertyFactory.Get(field.Tag, field.Type, field.Count, field.Data, BitConverterEx.SystemByteOrder, IFD.Zeroth, Encoding)); + var tag = ifd.Fields[j].Tag; + if (tag == 273 || tag == 279) + { + ifd.Fields.RemoveAt(j); + } } - } - #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]; + // 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)); - // Save the location of IFD offset - long ifdLocation = stream.Position - 4; + // Write fields after strips + ifdoffset = stripOffset; - // 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; - } + // Correct IFD offset + var currentLocation = stream.Position; + stream.Seek(ifdLocation, SeekOrigin.Begin); + stream.Write(conv.GetBytes(ifdoffset), 0, 4); + stream.Seek(currentLocation, SeekOrigin.Begin); - // 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)); + // Offset to field data + var dataOffset = ifdoffset + 2 + ((uint)ifd.Fields.Count * 12) + 4; - // Write fields after strips - ifdoffset = stripOffset; + // Field count + stream.Write(conv.GetBytes((ushort)ifd.Fields.Count), 0, 2); - // Correct IFD offset - long currentLocation = stream.Position; - stream.Seek(ifdLocation, SeekOrigin.Begin); - stream.Write(conv.GetBytes(ifdoffset), 0, 4); - stream.Seek(currentLocation, SeekOrigin.Begin); + // Fields + foreach (ImageFileDirectoryEntry field in ifd.Fields) + { + // Tag + stream.Write(conv.GetBytes(field.Tag), 0, 2); - // Offset to field data - uint dataOffset = ifdoffset + 2 + (uint)ifd.Fields.Count * 12 + 4; + // Type + stream.Write(conv.GetBytes(field.Type), 0, 2); - // Field count - stream.Write(conv.GetBytes((ushort)ifd.Fields.Count), 0, 2); + // Count + stream.Write(conv.GetBytes(field.Count), 0, 4); - // Fields - foreach (ImageFileDirectoryEntry field in ifd.Fields) + // Field data + var data = field.Data; + if (data.Length <= 4) { - // 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(data, 0, data.Length); + for (var j = data.Length; j < 4; j++) { - 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); + stream.WriteByte(0); } } - - // Offset to next IFD - ifdoffset = dataOffset; - stream.Write(conv.GetBytes(i == IFDs.Count - 1 ? 0 : ifdoffset), 0, 4); + 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); + } } - } - #endregion + // 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 ac7c503d0c9a..54a79d90b419 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 - { - /// - /// 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; + public BitConverterEx.ByteOrder ByteOrder; - /// - /// 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) - throw new NotValidTIFFHeader(); + /// + /// TIFF ID. This value should always be 42. + /// + public byte ID; - ByteOrder = byteOrder; - ID = id; - IFDOffset = ifdOffset; - TIFFHeaderByteOrder = headerByteOrder; - } + /// + /// The offset to the first IFD section from the + /// start of the TIFF header. + /// + public uint IFDOffset; - /// - /// 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) + /// + /// 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) { - TIFFHeader header = new TIFFHeader(); + throw new NotValidTIFFHeader(); + } - // 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(); + ByteOrder = byteOrder; + ID = id; + IFDOffset = ifdOffset; + TIFFHeaderByteOrder = headerByteOrder; + } - // 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; + /// + /// 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); - // IFD offset - header.IFDOffset = BitConverterEx.ToUInt32(data, offset + 4, header.TIFFHeaderByteOrder, BitConverterEx.SystemByteOrder); + // 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(); + } - return header; + // 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 9930961e207c..8bf91abde6ec 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 033b97ecc79d..1ce1b1cdc751 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); - - byte[] b = new byte[32768]; - int r; - while ((r = stream.Read(b, 0, b.Length)) > 0) - mem.Write(b, 0, r); + stream.Seek(0, SeekOrigin.Begin); - return mem.ToArray(); + var b = new byte[32768]; + int r; + while ((r = stream.Read(b, 0, b.Length)) > 0) + { + 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 e7937904bd6d..6760243ce6f1 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 2eaf632e54bd..67f11415d364 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 25bb1ac899e9..d8fdf72005ee 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 9cdc6869f411..ab904f20940f 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 +namespace Umbraco.Extensions; + +public static class ImageUrlGeneratorExtensions { - 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) { - /// - /// 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) { - if (imageUrlGenerator == null) - { - throw new ArgumentNullException(nameof(imageUrlGenerator)); - } - - return string.IsNullOrWhiteSpace(extension) == false && - imageUrlGenerator.SupportedImageFileTypes.InvariantContains(extension.TrimStart(Constants.CharArrays.Period)); + 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 b370efc1ae61..3e4834521da8 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 268fc1cd0d81..1903643d5e2b 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 0481323a4a15..e89d8e159d3a 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 167fbe5e0e6f..6f4e7a8a863c 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) - return null; + fileStream.Seek(0, SeekOrigin.Begin); + var header = new byte[8]; + fileStream.Seek(0, SeekOrigin.Begin); - return header; + // Invalid header + if (fileStream.Read(header, 0, header.Length) != header.Length) + { + return null; } + + return header; } } diff --git a/src/Umbraco.Core/Media/TypeDetector/SvgDetector.cs b/src/Umbraco.Core/Media/TypeDetector/SvgDetector.cs index 81f13b199d95..c790806b9b3d 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(); + var document = new XDocument(); - try - { - document = XDocument.Load(fileStream); - } - catch (System.Exception) - { - return false; - } - - return document.Root?.Name.LocalName == "svg"; + try + { + 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 1eda8efe7ac1..5581c81a6256 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"; - } + var tiffHeader = GetFileHeader(fileStream); + return (tiffHeader != null && tiffHeader == "MM\x00\x2a") || tiffHeader == "II\x2a\x00"; + } - public static string? GetFileHeader(Stream fileStream) + public static string? GetFileHeader(Stream fileStream) + { + var header = RasterizedTypeDetector.GetFileHeader(fileStream); + if (header == null) { - var header = RasterizedTypeDetector.GetFileHeader(fileStream); - if (header == null) - return null; - - var tiffHeader = Encoding.ASCII.GetString(header, 0, 4); - return tiffHeader; + return null; } + + 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 459866a8d97b..6a5ffd23d738 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 +namespace Umbraco.Cms.Core.Media; + +/// +/// 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)); + } + /// - /// Provides methods to manage auto-fill properties for upload fields. + /// Resets the auto-fill properties of a content item, for a specified auto-fill configuration. /// - public class UploadAutoFillProperties + /// The content item. + /// The auto-fill configuration. + /// Variation language. + /// Variation segment. + public void Reset(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string? culture, string? segment) { - private readonly MediaFileManager _mediaFileManager; - private readonly ILogger _logger; - private readonly IImageUrlGenerator _imageUrlGenerator; - private readonly IImageDimensionExtractor _imageDimensionExtractor; - - 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) throw new ArgumentNullException(nameof(content)); - if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } - ResetProperties(content, autoFillConfig, culture, segment); + if (autoFillConfig == null) + { + throw new ArgumentNullException(nameof(autoFillConfig)); } - /// - /// 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) + 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) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); + throw new ArgumentNullException(nameof(content)); + } - // no file = reset, file = auto-fill - if (filepath.IsNullOrWhiteSpace()) - { - ResetProperties(content, autoFillConfig, culture, segment); - } - else + if (autoFillConfig == null) + { + throw new ArgumentNullException(nameof(autoFillConfig)); + } + + // no file = reset, file = auto-fill + if (filepath.IsNullOrWhiteSpace()) + { + 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)) { - // 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)); + throw new ArgumentNullException(nameof(content)); + } - // no file = reset, file = auto-fill - if (filepath.IsNullOrWhiteSpace() || filestream == null) - { - ResetProperties(content, autoFillConfig, culture, segment); - } - else - { - SetProperties(content, autoFillConfig, filepath, filestream, culture, segment); - } + if (autoFillConfig == null) + { + throw new ArgumentNullException(nameof(autoFillConfig)); } - private void SetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string filepath, Stream? filestream, string? culture, string? segment) + // no file = reset, file = auto-fill + if (filepath.IsNullOrWhiteSpace() || filestream == null) + { + ResetProperties(content, autoFillConfig, culture, segment); + } + else { - var extension = (Path.GetExtension(filepath) ?? string.Empty).TrimStart(Constants.CharArrays.Period); + SetProperties(content, autoFillConfig, filepath, filestream, 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)); + } + + if (autoFillConfig == null) + { + throw new ArgumentNullException(nameof(autoFillConfig)); + } + + 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); + } - var size = _imageUrlGenerator.IsSupportedImageFormat(extension) - ? _imageDimensionExtractor.GetDimensions(filestream) ?? (Size?)new Size(Constants.Conventions.Media.DefaultSize, Constants.Conventions.Media.DefaultSize) - : null; + 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); + } - SetProperties(content, autoFillConfig, size, filestream?.Length, extension, culture, segment); + if (!string.IsNullOrWhiteSpace(autoFillConfig.LengthFieldAlias) && + content.Properties.Contains(autoFillConfig.LengthFieldAlias)) + { + content.Properties[autoFillConfig.LengthFieldAlias]!.SetValue(length, culture, segment); } - private static void SetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, Size? size, long? length, string extension, string? culture, string? segment) + if (!string.IsNullOrWhiteSpace(autoFillConfig.ExtensionFieldAlias) && + content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); + content.Properties[autoFillConfig.ExtensionFieldAlias]!.SetValue(extension, 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); + 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); - 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); + Size? size = _imageUrlGenerator.IsSupportedImageFormat(extension) + ? _imageDimensionExtractor.GetDimensions(filestream) ?? + (Size?)new Size(Constants.Conventions.Media.DefaultSize, Constants.Conventions.Media.DefaultSize) + : null; - if (!string.IsNullOrWhiteSpace(autoFillConfig.LengthFieldAlias) && content.Properties.Contains(autoFillConfig.LengthFieldAlias)) - content.Properties[autoFillConfig.LengthFieldAlias]!.SetValue(length, culture, segment); + SetProperties(content, autoFillConfig, size, filestream?.Length, extension, culture, segment); + } - if (!string.IsNullOrWhiteSpace(autoFillConfig.ExtensionFieldAlias) && content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) - content.Properties[autoFillConfig.ExtensionFieldAlias]!.SetValue(extension, culture, segment); + private static void ResetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, 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 (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.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.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); + 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 466751c82d0c..90faa01da1e6 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 e0bb52375b22..9d1b4dfcef04 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 int _performingUserId; - private string? _performingDetails; - private string? _performingIp; - private int _affectedUserId; - private string? _affectedDetails; - private string? _eventType; - private string? _eventDetails; + private string? _affectedDetails; + private int _affectedUserId; + private string? _eventDetails; + private string? _eventType; + private string? _performingDetails; + private string? _performingIp; + private int _performingUserId; - /// - public int PerformingUserId - { - 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 83ecad0878a1..bbfca724aa30 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(); - } + EnableChangeTracking(); + } - /// - public AuditType AuditType { get; } + /// + public AuditType AuditType { get; } - /// - public string? EntityType { get; } + /// + public string? EntityType { get; } - /// - public int UserId { get; } + /// + public int UserId { get; } - /// - public string? Comment { get; } + /// + public string? Comment { get; } - /// - public string? Parameters { get; } - } + /// + public string? Parameters { get; } } diff --git a/src/Umbraco.Core/Models/AuditType.cs b/src/Umbraco.Core/Models/AuditType.cs index b6a36be5ff03..6a3e528273bf 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. - /// - public enum AuditType - { - /// - /// New node(s) being added. - /// - New, - - /// - /// Node(s) being saved. - /// - Save, - - /// - /// Variant(s) being saved. - /// - SaveVariant, - - /// - /// Node(s) being opened. - /// - Open, - - /// - /// Node(s) being deleted. - /// - Delete, - - /// - /// Node(s) being published. - /// - Publish, - - /// - /// Variant(s) being published. - /// - PublishVariant, - - /// - /// Node(s) being sent to publishing. - /// - SendToPublish, - - /// - /// Variant(s) being sent to publishing. - /// - SendToPublishVariant, - - /// - /// Node(s) being unpublished. - /// - Unpublish, - - /// - /// Variant(s) being unpublished. - /// - UnpublishVariant, - - /// - /// Node(s) being moved. - /// - Move, - - /// - /// Node(s) being copied. - /// - Copy, - - /// - /// Node(s) being assigned domains. - /// - AssignDomain, - - /// - /// Node(s) public access changing. - /// - PublicAccess, - - /// - /// Node(s) being sorted. - /// - Sort, - - /// - /// Notification(s) being sent to user. - /// - Notify, - - /// - /// General system audit message. - /// - System, - - /// - /// Node's content being rolled back to a previous version. - /// - RollBack, - - /// - /// Package being installed. - /// - PackagerInstall, - - /// - /// Package being uninstalled. - /// - PackagerUninstall, - - /// - /// Custom audit message. - /// - Custom, - - /// - /// Content version preventCleanup set to true - /// - ContentVersionPreventCleanup, - - /// - /// Content version preventCleanup set to false - /// - ContentVersionEnableCleanup - } + /// New node(s) being added. + /// + New, + + /// + /// Node(s) being saved. + /// + Save, + + /// + /// Variant(s) being saved. + /// + SaveVariant, + + /// + /// Node(s) being opened. + /// + Open, + + /// + /// Node(s) being deleted. + /// + Delete, + + /// + /// Node(s) being published. + /// + Publish, + + /// + /// Variant(s) being published. + /// + PublishVariant, + + /// + /// Node(s) being sent to publishing. + /// + SendToPublish, + + /// + /// Variant(s) being sent to publishing. + /// + SendToPublishVariant, + + /// + /// Node(s) being unpublished. + /// + Unpublish, + + /// + /// Variant(s) being unpublished. + /// + UnpublishVariant, + + /// + /// Node(s) being moved. + /// + Move, + + /// + /// Node(s) being copied. + /// + Copy, + + /// + /// Node(s) being assigned domains. + /// + AssignDomain, + + /// + /// Node(s) public access changing. + /// + PublicAccess, + + /// + /// Node(s) being sorted. + /// + Sort, + + /// + /// Notification(s) being sent to user. + /// + Notify, + + /// + /// General system audit message. + /// + System, + + /// + /// Node's content being rolled back to a previous version. + /// + RollBack, + + /// + /// Package being installed. + /// + PackagerInstall, + + /// + /// Package being uninstalled. + /// + PackagerUninstall, + + /// + /// Custom audit message. + /// + Custom, + + /// + /// Content version preventCleanup set to true + /// + ContentVersionPreventCleanup, + + /// + /// Content version preventCleanup set to false + /// + ContentVersionEnableCleanup, } diff --git a/src/Umbraco.Core/Models/BackOfficeTour.cs b/src/Umbraco.Core/Models/BackOfficeTour.cs index d6a5d8971ee6..a7a9d3a5c3e4 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 21b769f94e5f..bc0a5cea3bc5 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(); - } - - /// - /// The file name for the tour - /// - [DataMember(Name = "fileName")] - public string? FileName { get; set; } + [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 aa2aaf7f533c..296dcf8bc400 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 400649ff0541..ee158f9bd8c9 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; - } + 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 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 UDI. - /// - /// - /// The settings UDI. - /// - [DataMember(Name = "settingsUdi")] - public Udi SettingsUdi { 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; } - } + /// + /// 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 +{ /// - /// Represents a layout item with a generic content type for the Block List editor. + /// Initializes a new instance of the class. /// - /// 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; - } + /// 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; } - } + /// + /// 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 +{ /// - /// Represents a layout item with generic content and settings types for the Block List editor. + /// Initializes a new instance of the class. /// - /// 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 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; } - } + /// + /// 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 33a711520b4b..79afb67d40d2 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()) - { } + /// + /// 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 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 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; - } + /// + /// 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 f8677490ee74..96a81641fa5c 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; - } + ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); + SettingsUdi = settingsUdi; + } - public Udi ContentUdi { get; } + public Udi ContentUdi { get; } - public Udi? SettingsUdi { get; } + public Udi? SettingsUdi { get; } - public override bool Equals(object? obj) => obj is ContentAndSettingsReference reference && Equals(reference); + public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right) => + left.Equals(right); - public bool Equals(ContentAndSettingsReference other) => other != null - && EqualityComparer.Default.Equals(ContentUdi, other.ContentUdi) - && EqualityComparer.Default.Equals(SettingsUdi, other.SettingsUdi); + public override bool Equals(object? obj) => obj is ContentAndSettingsReference reference && Equals(reference); - public override int GetHashCode() => (ContentUdi, SettingsUdi).GetHashCode(); + public bool Equals(ContentAndSettingsReference other) => other != null + && EqualityComparer.Default.Equals( + ContentUdi, + other.ContentUdi) + && EqualityComparer.Default.Equals( + SettingsUdi, + other.SettingsUdi); - public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right) - { - return left.Equals(right); - } + public override int GetHashCode() => (ContentUdi, SettingsUdi).GetHashCode(); - public static bool operator !=(ContentAndSettingsReference left, ContentAndSettingsReference right) - { - return !(left == right); - } - } + 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 48c2b8563760..44533407d243 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; } - } + /// + /// 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 +{ /// - /// Represents a data item reference with settings for a Block editor implementation. + /// Gets the settings. /// - /// 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 settings. + /// + TSettings Settings { get; } } diff --git a/src/Umbraco.Core/Models/CacheInstruction.cs b/src/Umbraco.Core/Models/CacheInstruction.cs index 5434f443a0fd..a93ec030c8f0 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; } + Id = id; + UtcStamp = utcStamp; + Instructions = instructions; + OriginIdentity = originIdentity; + InstructionCount = instructionCount; + } - /// - /// Cache instruction created date. - /// - public DateTime UtcStamp { get; } + /// + /// Cache instruction Id. + /// + public int Id { get; } - /// - /// Serialized instructions. - /// - public string Instructions { get; } + /// + /// Cache instruction created date. + /// + public DateTime UtcStamp { get; } - /// - /// Identity of server originating the instruction. - /// - public string OriginIdentity { get; } + /// + /// Serialized instructions. + /// + public string Instructions { get; } - /// - /// Count of instructions. - /// - public int InstructionCount { 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 be19f13b752c..946bcde9abc0 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 2354c67b1e38..e71f040ba8e9 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 - { - private bool _current; - private string? _source; - private string? _context; - private string? _action; - private ConsentState _state; - private string? _comment; + public List? HistoryInternal { get; set; } - /// - public bool Current - { - get => _current; - set => SetPropertyValueAndDetectChanges(value, ref _current, nameof(Current)); - } + /// + public bool Current + { + get => _current; + set => SetPropertyValueAndDetectChanges(value, ref _current, nameof(Current)); + } - /// - public string? Source + /// + public string? Source + { + get => _source; + set { - get => _source; - set + if (string.IsNullOrWhiteSpace(value)) { - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _source, nameof(Source)); + throw new ArgumentException(nameof(value)); } + + SetPropertyValueAndDetectChanges(value, ref _source, nameof(Source)); } + } - /// - public string? Context + /// + public string? Context + { + get => _context; + set { - get => _context; - set + if (string.IsNullOrWhiteSpace(value)) { - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _context, nameof(Context)); + throw new ArgumentException(nameof(value)); } + + SetPropertyValueAndDetectChanges(value, ref _context, nameof(Context)); } + } - /// - public string? Action + /// + public string? Action + { + get => _action; + set { - get => _action; - set + if (string.IsNullOrWhiteSpace(value)) { - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _action, nameof(Action)); + throw new ArgumentException(nameof(value)); } - } - /// - 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)); + SetPropertyValueAndDetectChanges(value, ref _action, nameof(Action)); } + } - /// - public string? Comment - { - get => _comment; - set => SetPropertyValueAndDetectChanges(value, ref _comment, nameof(Comment)); - } + /// + public ConsentState State + { + get => _state; - /// - public IEnumerable? History => HistoryInternal; + // 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)); + } - /// - /// Gets the previous states of this consent. - /// - public List? HistoryInternal { get; set; } + /// + 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 b95c7b66f980..1dc6cadde803 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 0828561ff833..8a6846b28c42 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 - - /// - /// There is no consent. - /// - None = 0, + 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 bc77e52624b8..4e251a323ec8 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 - - 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) - { - CreatorId = userId; - WriterId = userId; - } + } - /// - /// 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) throw new ArgumentNullException(nameof(contentType)); - _publishedState = PublishedState.Unpublished; - PublishedVersionId = 0; - } + /// + /// 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; + } - /// - /// 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) + /// + /// 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 - /// 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; - } + _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) + { + } - /// - /// 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 + /// + /// 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) { - get => _templateId; - set => SetPropertyValueAndDetectChanges(value, ref _templateId, nameof(TemplateId)); + throw new ArgumentNullException(nameof(contentType)); } - /// - /// 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 + _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 { - get => _published; - set - { - SetPropertyValueAndDetectChanges(value, ref _published, nameof(Published)); - _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; - } + 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 + /// + /// 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 { - get => _publishedState; - set + if (value != PublishedState.Publishing && value != PublishedState.Unpublishing) { - if (value != PublishedState.Publishing && value != PublishedState.Unpublishing) - throw new ArgumentException("Invalid state, only Publishing and Unpublishing are accepted."); - _publishedState = value; + throw new ArgumentException("Invalid state, only Publishing and Unpublishing are accepted."); } + + _publishedState = value; } + } - [IgnoreDataMember] - public bool Edited { get; set; } + [IgnoreDataMember] + public bool Edited { get; set; } - /// - [IgnoreDataMember] - public DateTime? PublishDate { get; set; } // set by persistence + /// + [IgnoreDataMember] + public DateTime? PublishDate { get; set; } // set by persistence - /// - [IgnoreDataMember] - public int? PublisherId { 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 int? PublishTemplateId { get; set; } // set by persistence - /// - [IgnoreDataMember] - public string? PublishName { 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? 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 + /// + [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 { - get + if (_publishInfos != null) { - if (_publishInfos != null) return _publishInfos; - _publishInfos = new ContentCultureInfosCollection(); - _publishInfos.CollectionChanged += PublishNamesCollectionChanged; return _publishInfos; } - set + + _publishInfos = new ContentCultureInfosCollection(); + _publishInfos.CollectionChanged += PublishNamesCollectionChanged; + return _publishInfos; + } + + set + { + if (_publishInfos != null) { - if (_publishInfos != null) - { - _publishInfos.ClearCollectionChangedEvents(); - } + _publishInfos.ClearCollectionChangedEvents(); + } - _publishInfos = value; - if (_publishInfos != null) - { - _publishInfos.CollectionChanged += PublishNamesCollectionChanged; - } + _publishInfos = value; + if (_publishInfos != null) + { + _publishInfos.CollectionChanged += PublishNamesCollectionChanged; } } + } - /// - public string? GetPublishName(string? culture) + /// + public string? GetPublishName(string? culture) + { + if (culture.IsNullOrWhiteSpace()) { - 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; + return PublishName; } - /// - public DateTime? GetPublishDate(string culture) + if (!ContentType.VariesByCulture()) { - 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; + return null; } - /// - /// Handles culture infos collection changes. - /// - private void PublishNamesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + if (_publishInfos == null) { - 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; - } - } + return null; } - [IgnoreDataMember] - public int PublishedVersionId { get; set; } + return _publishInfos.TryGetValue(culture!, out ContentCultureInfos infos) ? infos.Name : null; + } - [DataMember] - public bool Blueprint { get; set; } + /// + public DateTime? GetPublishDate(string culture) + { + if (culture.IsNullOrWhiteSpace()) + { + return PublishDate; + } - /// - /// Changes the for the current content object - /// - /// New ContentType for this content - /// Leaves PropertyTypes intact after change - internal void ChangeContentType(IContentType contentType) + if (!ContentType.VariesByCulture()) { - ChangeContentType(contentType, false); + return null; } - /// - /// 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) + if (_publishInfos == null) { - ChangeContentType(new SimpleContentType(contentType)); + return null; + } - if (clearProperties) - Properties.EnsureCleanPropertyTypes(contentType.CompositionPropertyTypes); - else - Properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes); + return _publishInfos.TryGetValue(culture, out ContentCultureInfos infos) ? infos.Date : null; + } - Properties.ClearCollectionChangedEvents(); // be sure not to double add - Properties.CollectionChanged += PropertiesChanged; - } + [IgnoreDataMember] + public int PublishedVersionId { get; set; } + + [DataMember] + public bool Blueprint { get; set; } - public override void ResetWereDirtyProperties() + 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 { - base.ResetWereDirtyProperties(); - _previousPublishCultureChanges.updatedCultures = null; - _previousPublishCultureChanges.removedCultures = null; _previousPublishCultureChanges.addedCultures = null; + _previousPublishCultureChanges.removedCultures = null; + _previousPublishCultureChanges.updatedCultures = null; } - public override void ResetDirtyProperties(bool rememberDirty) + _currentPublishCultureChanges.addedCultures?.Clear(); + _currentPublishCultureChanges.removedCultures?.Clear(); + _currentPublishCultureChanges.updatedCultures?.Clear(); + + // take care of the published state + _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; + + if (_publishInfos == null) { - base.ResetDirtyProperties(rememberDirty); + return; + } - 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(); + foreach (ContentCultureInfos infos in _publishInfos) + { + infos.ResetDirtyProperties(rememberDirty); + } + } - // take care of the published state - _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; + /// + /// 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 (_publishInfos == null) return; + if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture); + return _currentPublishCultureChanges.removedCultures?.Contains(culture) ?? false; + } - foreach (var infos in _publishInfos) - infos.ResetDirtyProperties(rememberDirty); + if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.ChangedCulture); + return _currentPublishCultureChanges.updatedCultures?.Contains(culture) ?? false; } - /// - /// Overridden to check special keys. - public override bool IsPropertyDirty(string propertyName) + 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)) { - //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; - } + var culture = propertyName.TrimStart(ChangeTrackingPrefix.PublishedCulture); + return _previousPublishCultureChanges.addedCultures?.Contains(culture) ?? false; + } - return base.IsPropertyDirty(propertyName); + if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture); + return _previousPublishCultureChanges.removedCultures?.Contains(culture) ?? false; } - /// - /// Overridden to check special keys. - public override bool WasPropertyDirty(string propertyName) + if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture)) { - //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.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: { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.PublishedCulture); - return _previousPublishCultureChanges.addedCultures?.Contains(culture) ?? false; + 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; } - if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture)) + + case NotifyCollectionChangedAction.Remove: { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture); - return _previousPublishCultureChanges.removedCultures?.Contains(culture) ?? false; + // 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; } - if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture)) + + case NotifyCollectionChangedAction.Replace: { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.ChangedCulture); - return _previousPublishCultureChanges.updatedCultures?.Contains(culture) ?? false; - } + // 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); + } - return base.WasPropertyDirty(propertyName); + break; + } } + } - /// - /// 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(); + /// + /// 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); - foreach (var property in clone.Properties) - property.ResetIdentity(); + /// + /// 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)); - return clone; + if (clearProperties) + { + Properties.EnsureCleanPropertyTypes(contentType.CompositionPropertyTypes); } - - protected override void PerformDeepClone(object clone) + else { - base.PerformDeepClone(clone); + Properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes); + } - var clonedContent = (Content)clone; + Properties.ClearCollectionChangedEvents(); // be sure not to double add + Properties.CollectionChanged += PropertiesChanged; + } + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); - //fixme - need to reset change tracking bits + var clonedContent = (Content)clone; - //if culture infos exist then deal with event bindings - if (clonedContent._publishInfos != null) + // 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) { - 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 - } + // Re-assign correct event handler + clonedContent._publishInfos.CollectionChanged += clonedContent.PublishNamesCollectionChanged; } + } - clonedContent._currentPublishCultureChanges.updatedCultures = null; - clonedContent._currentPublishCultureChanges.addedCultures = null; - clonedContent._currentPublishCultureChanges.removedCultures = null; + clonedContent._currentPublishCultureChanges.updatedCultures = null; + clonedContent._currentPublishCultureChanges.addedCultures = null; + clonedContent._currentPublishCultureChanges.removedCultures = null; - clonedContent._previousPublishCultureChanges.updatedCultures = null; - clonedContent._previousPublishCultureChanges.addedCultures = null; - clonedContent._previousPublishCultureChanges.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 d9223130d631..e9fcc61e7c91 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); + // 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; } + private set => SetPropertyValueAndDetectChanges(value, ref _contentTypeId, nameof(ContentTypeId)); + } - private ContentBase(string? name, IContentTypeComposition? contentType, IPropertyCollection properties, string? culture = null) + /// + /// 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 { - ContentType = contentType?.ToSimple() ?? throw new ArgumentNullException(nameof(contentType)); + if (_properties != null) + { + _properties.ClearCollectionChangedEvents(); + } - // initially, all new instances have - Id = 0; // no identity - VersionId = 0; // no versions + _properties = value; + _properties.CollectionChanged += PropertiesChanged; + } + } - SetCultureName(name, culture); + public void ChangeContentType(ISimpleContentType contentType) + { + ContentType = contentType; + ContentTypeId = contentType.Id; + } - _contentTypeId = contentType.Id; - _properties = properties ?? throw new ArgumentNullException(nameof(properties)); - _properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes); + protected void PropertiesChanged(object? sender, NotifyCollectionChangedEventArgs e) => + OnPropertyChanged(nameof(Properties)); - //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); - } + /// + /// + /// Overridden to deal with specific object instances + /// + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); - [IgnoreDataMember] - public ISimpleContentType ContentType { get; private set; } + var clonedContent = (ContentBase)clone; - public void ChangeContentType(ISimpleContentType contentType) - { - ContentType = contentType; - ContentTypeId = contentType.Id; - } + // Need to manually clone this since it's not settable + clonedContent.ContentType = ContentType; - protected void PropertiesChanged(object? sender, NotifyCollectionChangedEventArgs e) + // If culture infos exist then deal with event bindings + if (clonedContent._cultureInfos != null) { - OnPropertyChanged(nameof(Properties)); + 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 + } } - /// - /// Id of the user who wrote/updated this entity - /// - [DataMember] - public int WriterId + // if properties exist then deal with event bindings + if (clonedContent._properties != null) { - get => _writerId; - set => SetPropertyValueAndDetectChanges(value, ref _writerId, nameof(WriterId)); + 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 } - [IgnoreDataMember] - public int VersionId { get; set; } + 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); - /// - /// Integer Id of the default ContentType - /// - [DataMember] - public int ContentTypeId + /// + [DataMember] + public ContentCultureInfosCollection? CultureInfos + { + get { - get + if (_cultureInfos != 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; + return _cultureInfos; } - private set => SetPropertyValueAndDetectChanges(value, ref _contentTypeId, nameof(ContentTypeId)); + + _cultureInfos = new ContentCultureInfosCollection(); + _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; + return _cultureInfos; } - /// - /// 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 + set { - get => _properties; - set + if (_cultureInfos != null) { - if (_properties != null) - { - _properties.ClearCollectionChangedEvents(); - } + _cultureInfos.ClearCollectionChangedEvents(); + } - _properties = value; - _properties.CollectionChanged += PropertiesChanged; + _cultureInfos = value; + if (_cultureInfos != null) + { + _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; } } + } + + /// + public string? GetCultureName(string? culture) + { + if (culture.IsNullOrWhiteSpace()) + { + return Name; + } + + if (!ContentType.VariesByCulture()) + { + return null; + } + + if (_cultureInfos == null) + { + return null; + } - #region Cultures + return _cultureInfos.TryGetValue(culture!, out ContentCultureInfos infos) ? infos.Name : null; + } - // 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 DateTime? GetUpdateDate(string culture) + { + if (culture.IsNullOrWhiteSpace()) + { + return null; + } + + if (!ContentType.VariesByCulture()) + { + return null; + } - /// - public IEnumerable AvailableCultures - => _cultureInfos?.Keys ?? Enumerable.Empty(); + if (_cultureInfos == null) + { + return null; + } - /// - public bool IsCultureAvailable(string culture) - => _cultureInfos != null && _cultureInfos.ContainsKey(culture); + return _cultureInfos.TryGetValue(culture, out ContentCultureInfos infos) ? infos.Date : null; + } - /// - [DataMember] - public ContentCultureInfosCollection? CultureInfos + /// + public void SetCultureName(string? name, string? culture) + { + // set on variant content type + if (ContentType.VariesByCulture()) { - get + // invariant is ok + if (culture.IsNullOrWhiteSpace()) { - if (_cultureInfos != null) return _cultureInfos; - _cultureInfos = new ContentCultureInfosCollection(); - _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; - return _cultureInfos; + Name = name; // may be null } - set + + // clear + else if (name.IsNullOrWhiteSpace()) { - if (_cultureInfos != null) - { - _cultureInfos.ClearCollectionChangedEvents(); - } - _cultureInfos = value; - if (_cultureInfos != null) - { - _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; - } + ClearCultureInfo(culture!); + } + + // set + else + { + this.SetCultureInfo(culture!, name, DateTime.Now); + } + } + + // set on invariant content type + else + { + // 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 string? GetCultureName(string? culture) + if (string.IsNullOrWhiteSpace(culture)) { - 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; + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); } - /// - public DateTime? GetUpdateDate(string culture) + if (_cultureInfos == null) { - 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; } - /// - public void SetCultureName(string? name, string? culture) + _cultureInfos.Remove(culture); + if (_cultureInfos.Count == 0) { - if (ContentType.VariesByCulture()) // set on variant content type + _cultureInfos = null; + } + } + + /// + /// Handles culture infos collection changes. + /// + private void CultureInfosCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(CultureInfos)); + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: { - if (culture.IsNullOrWhiteSpace()) // invariant is ok + ContentCultureInfos? cultureInfo = e.NewItems?.Cast().First(); + if (_currentCultureChanges.addedCultures == null) { - Name = name; // may be null + _currentCultureChanges.addedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); } - else if (name.IsNullOrWhiteSpace()) // clear + + if (_currentCultureChanges.updatedCultures == null) { - ClearCultureInfo(culture!); + _currentCultureChanges.updatedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); } - else // set + + if (cultureInfo is not null) { - this.SetCultureInfo(culture!, name, DateTime.Now); + _currentCultureChanges.addedCultures.Add(cultureInfo.Culture); + _currentCultureChanges.updatedCultures.Add(cultureInfo.Culture); + _currentCultureChanges.removedCultures?.Remove(cultureInfo.Culture); } - } - else // set on invariant content type - { - if (!culture.IsNullOrWhiteSpace()) // invariant is NOT ok - throw new NotSupportedException("Content type does not vary by culture."); - Name = name; // may be null + break; } - } - 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)); + case NotifyCollectionChangedAction.Remove: + { + // Remove listening for changes + ContentCultureInfos? cultureInfo = e.OldItems?.Cast().First(); + if (_currentCultureChanges.removedCultures == null) + { + _currentCultureChanges.removedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } - if (_cultureInfos == null) return; - _cultureInfos.Remove(culture); - if (_cultureInfos.Count == 0) - _cultureInfos = null; - } + if (cultureInfo is not null) + { + _currentCultureChanges.removedCultures.Add(cultureInfo.Culture); + _currentCultureChanges.updatedCultures?.Remove(cultureInfo.Culture); + _currentCultureChanges.addedCultures?.Remove(cultureInfo.Culture); + } - /// - /// Handles culture infos collection changes. - /// - private void CultureInfosCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(CultureInfos)); + break; + } - switch (e.Action) + case NotifyCollectionChangedAction.Replace: { - 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); - } - - 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; - } + // 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 + #endregion - #region Has, Get, Set, Publish Property Value + #region Has, Get, Set, Publish Property Value - /// - public bool HasProperty(string propertyTypeAlias) - => Properties.Contains(propertyTypeAlias); + /// + public bool HasProperty(string propertyTypeAlias) + => Properties.Contains(propertyTypeAlias); - /// - public object? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false) + /// + 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 Properties.TryGetValue(propertyTypeAlias, out var property) - ? property?.GetValue(culture, segment, published) - : null; + return default; } - /// - public TValue? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false) - { - if (!Properties.TryGetValue(propertyTypeAlias, out var property)) - return default; + Attempt? convertAttempt = property?.GetValue(culture, segment, published).TryConvertTo(); + return convertAttempt?.Success is not null && (convertAttempt?.Success ?? false) + ? convertAttempt.Value.Result + : default; + } - var 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}\"."); } - /// - public void SetValue(string propertyTypeAlias, object? value, string? culture = null, string? segment = null) - { - if (!Properties.TryGetValue(propertyTypeAlias, out var property)) - throw new InvalidOperationException($"No PropertyType exists with the supplied alias \"{propertyTypeAlias}\"."); + property?.SetValue(value, culture, segment); - property?.SetValue(value, culture, segment); + // bump the culture to be flagged for updating + this.TouchCulture(culture); + } - //bump the culture to be flagged for updating - this.TouchCulture(culture); - } + #endregion - #endregion + #region Dirty - #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); - public override void ResetWereDirtyProperties() + 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) - { - 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); + _currentCultureChanges.addedCultures?.Clear(); + _currentCultureChanges.removedCultures?.Clear(); + _currentCultureChanges.updatedCultures?.Clear(); - // take care of culture infos - if (_cultureInfos == null) return; - - foreach (var cultureInfo in _cultureInfos) - cultureInfo.ResetDirtyProperties(rememberDirty); + // also reset dirty changes made to user's properties + foreach (IProperty prop in Properties) + { + 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(); + cultureInfo.ResetDirtyProperties(rememberDirty); } + } + + /// + /// Overridden to include user properties. + public override bool IsDirty() => IsEntityDirty() || this.IsAnyUserPropertyDirty(); - /// - /// Gets a value indicating whether the current entity's own properties (not user) are dirty. - /// - public bool IsEntityDirty() + /// + /// 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 base.IsDirty(); + return true; } - /// - /// Gets a value indicating whether the current entity's own properties (not user) were dirty. - /// - public bool WasEntityDirty() + // Special check here since we want to check if the request is for changed cultures + if (propertyName.StartsWith(ChangeTrackingPrefix.AddedCulture)) { - return base.WasDirty(); + var culture = propertyName.TrimStart(ChangeTrackingPrefix.AddedCulture); + return _currentCultureChanges.addedCultures?.Contains(culture) ?? false; } - /// - /// Overridden to include user properties. - public override bool IsPropertyDirty(string propertyName) + if (propertyName.StartsWith(ChangeTrackingPrefix.RemovedCulture)) { - 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); + var culture = propertyName.TrimStart(ChangeTrackingPrefix.RemovedCulture); + return _currentCultureChanges.removedCultures?.Contains(culture) ?? false; } - /// - /// Overridden to include user properties. - public override bool WasPropertyDirty(string propertyName) + if (propertyName.StartsWith(ChangeTrackingPrefix.UpdatedCulture)) { - if (base.WasPropertyDirty(propertyName)) - return true; + var culture = propertyName.TrimStart(ChangeTrackingPrefix.UpdatedCulture); + return _currentCultureChanges.updatedCultures?.Contains(culture) ?? false; + } - //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]?.IsDirty() ?? false); + } - return Properties.Contains(propertyName) && (Properties[propertyName]?.WasDirty() ?? false); + /// + /// Overridden to include user properties. + public override bool WasPropertyDirty(string propertyName) + { + if (base.WasPropertyDirty(propertyName)) + { + return true; } - /// - /// Overridden to include user properties. - public override IEnumerable GetDirtyProperties() + // Special check here since we want to check if the request is for changed cultures + if (propertyName.StartsWith(ChangeTrackingPrefix.AddedCulture)) { - var instanceProperties = base.GetDirtyProperties(); - var propertyTypes = Properties.Where(x => x.IsDirty()).Select(x => x.Alias); - return instanceProperties.Concat(propertyTypes); + var culture = propertyName.TrimStart(ChangeTrackingPrefix.AddedCulture); + return _previousCultureChanges.addedCultures?.Contains(culture) ?? false; } - /// - /// Overridden to include user properties. - public override IEnumerable GetWereDirtyProperties() + if (propertyName.StartsWith(ChangeTrackingPrefix.RemovedCulture)) { - var instanceProperties = base.GetWereDirtyProperties(); - var propertyTypes = Properties.Where(x => x.WasDirty()).Select(x => x.Alias); - return instanceProperties.Concat(propertyTypes); + var culture = propertyName.TrimStart(ChangeTrackingPrefix.RemovedCulture); + return _previousCultureChanges.removedCultures?.Contains(culture) ?? false; } - #endregion - - /// - /// - /// Overridden to deal with specific object instances - /// - protected override void PerformDeepClone(object clone) + if (propertyName.StartsWith(ChangeTrackingPrefix.UpdatedCulture)) { - 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 - } - } + var culture = propertyName.TrimStart(ChangeTrackingPrefix.UpdatedCulture); + return _previousCultureChanges.updatedCultures?.Contains(culture) ?? false; + } - //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 - } + return Properties.Contains(propertyName) && (Properties[propertyName]?.WasDirty() ?? false); + } - clonedContent._currentCultureChanges.updatedCultures = null; - clonedContent._currentCultureChanges.addedCultures = null; - clonedContent._currentCultureChanges.removedCultures = null; + /// + /// 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); + } - clonedContent._previousCultureChanges.updatedCultures = null; - clonedContent._previousCultureChanges.addedCultures = null; - clonedContent._previousCultureChanges.removedCultures = null; - } + /// + /// 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 b81cf398bfc5..656db0f82f39 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)); + throw new ArgumentNullException(nameof(content)); + } - 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); - } + if (urlSegmentProviders == null) + { + throw new ArgumentNullException(nameof(urlSegmentProviders)); + } - url = s_defaultUrlSegmentProvider.GetUrlSegment(content, culture); // be safe + var url = urlSegmentProviders.Select(p => p.GetUrlSegment(content, culture)).FirstOrDefault(u => u != null); + if (url == null) + { + if (_defaultUrlSegmentProvider == null) + { + _defaultUrlSegmentProvider = new DefaultUrlSegmentProvider(shortStringHelper); } - return url; + url = _defaultUrlSegmentProvider.GetUrlSegment(content, culture); // be safe } - private static DefaultUrlSegmentProvider? s_defaultUrlSegmentProvider; + return url; } } diff --git a/src/Umbraco.Core/Models/ContentCultureInfos.cs b/src/Umbraco.Core/Models/ContentCultureInfos.cs index 47f0765d6344..8975c1fc586f 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 - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(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 date. - /// - public DateTime Date - { - get => _date; - set => SetPropertyValueAndDetectChanges(value, ref _date, nameof(Date)); - } + /// + /// Gets the culture. + /// + public string Culture { get; } - /// - public object DeepClone() - { - return new ContentCultureInfos(this); - } + /// + /// Gets the name. + /// + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } - /// - public override bool Equals(object? obj) - { - return obj is ContentCultureInfos other && Equals(other); - } + /// + /// Gets the date. + /// + public DateTime Date + { + get => _date; + set => SetPropertyValueAndDetectChanges(value, ref _date, nameof(Date)); + } - /// - public bool Equals(ContentCultureInfos? other) - { - return other != null && Culture == other.Culture && Name == other.Name; - } + /// + public object DeepClone() => new ContentCultureInfos(this); - /// - 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; - } + /// + public bool Equals(ContentCultureInfos? other) => other != null && Culture == other.Culture && Name == other.Name; - /// - /// Deconstructs into culture and name. - /// - public void Deconstruct(out string culture, out string? name) - { - culture = Culture; - name = Name; - } + /// + public override bool Equals(object? obj) => obj is ContentCultureInfos other && Equals(other); - /// - /// Deconstructs into culture, name and date. - /// - public void Deconstruct(out string culture, out string? name, out DateTime date) + /// + public override int GetHashCode() + { + var hashCode = 479558943; + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(Culture); + if (Name is not null) { - Deconstruct(out culture, out name); - date = Date; + 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; } } diff --git a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs index 0d480ede6db5..cf8a2f03288a 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) + { + throw new ArgumentNullException(nameof(culture)); + } + + if (string.IsNullOrWhiteSpace(culture)) { - var clone = new ContentCultureInfosCollection(); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(culture)); + } - foreach (var item in this) - { - var itemClone = (ContentCultureInfos) item.DeepClone(); - itemClone.ResetDirtyProperties(false); - clone.Add(itemClone); - } + culture = culture.ToLowerInvariant(); - return clone; + 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 8a13a26e402a..f4ad0b0dfcef 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; - } - - 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); - - 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 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, } + + 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 e6138addbc42..fe0ebce5492d 100644 --- a/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs @@ -1,13 +1,10 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public class ContentDataIntegrityReportEntry { - public class ContentDataIntegrityReportEntry - { - public ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType issueType) - { - IssueType = issueType; - } + public ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType issueType) => IssueType = issueType; + + public ContentDataIntegrityReport.IssueType IssueType { get; } - public ContentDataIntegrityReport.IssueType IssueType { get; } - public bool Fixed { get; set; } - } + public bool Fixed { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs index 52ea3d403218..40657069ffcb 100644 --- a/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public class ContentDataIntegrityReportOptions { - public class ContentDataIntegrityReportOptions - { - /// - /// Set to true to try to automatically resolve data integrity issues - /// - public bool FixIssues { get; set; } + /// + /// 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... - } + // 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 2c73bcd5904f..18229d2124e9 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 b6aca055155b..867784d19df2 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 - { - /// - /// The assigned permissions for the user group organized by permission group name - /// - [DataMember(Name = "permissions")] - public IDictionary>? AssignedPermissions { get; set; } + [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; } + /// + /// 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) + 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 5f40ace6ca29..e7b744bd5907 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 982f46d91236..1cf1e60e2552 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; - } + public BackOfficeNotification(string header, string message, NotificationStyle notificationType) + { + Header = header; + Message = message; + NotificationType = notificationType; + } - [DataMember(Name = "header")] - public string? Header { get; set; } + [DataMember(Name = "header")] + public string? Header { get; set; } - [DataMember(Name = "message")] - public string? Message { get; set; } + [DataMember(Name = "message")] + public string? Message { get; set; } - [DataMember(Name = "type")] - public NotificationStyle NotificationType { 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 37243c702cb2..b172fccb5a1f 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(); - } + 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; } + /// + /// 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; } + /// + /// 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 = "name", IsRequired = true)] + public string? Name { get; set; } - [DataMember(Name = "content", IsRequired = true)] - public string? Content { get; set; } + [DataMember(Name = "content", IsRequired = true)] + public string? Content { get; set; } - [DataMember(Name = "fileType", IsRequired = true)] - public string? FileType { get; set; } + [DataMember(Name = "fileType", IsRequired = true)] + public string? FileType { get; set; } - [DataMember(Name = "snippet")] - [ReadOnly(true)] - public string? Snippet { get; set; } + [DataMember(Name = "snippet")] + [ReadOnly(true)] + public string? Snippet { get; set; } - [DataMember(Name = "id")] - [ReadOnly(true)] - public string? Id { get; set; } + [DataMember(Name = "id")] + [ReadOnly(true)] + public string? Id { get; set; } - public List Notifications { get; private set; } + public List Notifications { get; } - /// - /// Some custom validation is required for valid file names - /// - /// - /// - public IEnumerable Validate(ValidationContext validationContext) + /// + /// Some custom validation is required for valid file names + /// + /// + /// + public IEnumerable Validate(ValidationContext validationContext) + { + 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()) { - 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 3dc88df3ddd2..02c32adc540c 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; } - - /// - /// 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; } + [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 4e1089c97b5b..7a50073d1717 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 9bcadd13831b..718b36db3358 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 -{ - // 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? +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 - { - [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 d7f026aeab6e..241cde46b46f 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 86af3de89a03..ca24b08567b2 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 +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "ContentDomainsAndCulture")] +public class ContentDomainsAndCulture { - [DataContract(Name = "ContentDomainsAndCulture")] - public class ContentDomainsAndCulture - { - [DataMember(Name = "domains")] - public IEnumerable? Domains { get; set; } + [DataMember(Name = "domains")] + public IEnumerable? Domains { get; set; } - [DataMember(Name = "language")] - public string? Language { 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 9b1fcdc12907..fd277308f74f 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 { - /// - /// 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 = "updateDate")] + public DateTime UpdateDate { get; set; } - [DataMember(Name = "createDate")] - public DateTime CreateDate { 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; + /// + /// 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; } + /// + /// 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 = "owner")] + public UserProfile? Owner { get; set; } - [DataMember(Name = "updater")] - public UserProfile? Updater { get; set; } + [DataMember(Name = "updater")] + public UserProfile? Updater { get; set; } - public int ContentTypeId { get; set; } + public int ContentTypeId { get; set; } - [DataMember(Name = "contentTypeAlias", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string ContentTypeAlias { get; set; } = null!; + [DataMember(Name = "contentTypeAlias", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string ContentTypeAlias { get; set; } = null!; - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } + [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; } + /// + /// 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; } + [DataMember(Name = "variesByCulture")] + public bool VariesByCulture { get; set; } - protected bool Equals(ContentItemBasic other) + public override bool Equals(object? obj) + { + 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; + return base.GetHashCode(); + } +} - [DataMember(Name = "properties")] - public virtual IEnumerable Properties - { - get => _properties; - set => _properties = value; - } +/// +/// 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 874f2f085a7f..e2fcf71053ac 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 +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public class ContentItemDisplay : ContentItemDisplay { - public class ContentItemDisplay : ContentItemDisplay { } +} - public class ContentItemDisplayWithSchedule : 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 +/// +/// 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() { - 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; } - - /// - /// 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; } - - /// - /// The path of the entity - /// - [DataMember(Name = "path")] - public string? Path { 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 = "owner")] - public UserProfile? Owner { get; set; } - - [DataMember(Name = "updater")] - public UserProfile? Updater { get; set; } - - /// - /// 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; } - - /// - /// Indicates if the content is configured as an element - /// - [DataMember(Name = "isElement")] - public bool IsElement { get; set; } - - /// - /// Property indicating if this item is part of a list view parent - /// - [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; } - - [DataMember(Name = "contentTypeId")] - public int? ContentTypeId { get; set; } - - [DataMember(Name = "contentTypeKey")] - public Guid ContentTypeKey { get; set; } - - [DataMember(Name = "contentTypeAlias", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string ContentTypeAlias { get; set; } = null!; - - [DataMember(Name = "sortOrder")] - public int SortOrder { 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 = "template")] - public string? TemplateAlias { get; set; } - - [DataMember(Name = "templateId")] - public int TemplateId { get; set; } - - [DataMember(Name = "allowedTemplates")] - public IDictionary? AllowedTemplates { get; set; } - - [DataMember(Name = "documentType")] - public ContentTypeBasic? DocumentType { get; set; } - - [DataMember(Name = "urls")] - public UrlInfo[]? Urls { 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 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 = "isBlueprint")] - public bool IsBlueprint { 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; } + 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; } + + /// + /// 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; } + + /// + /// The path of the entity + /// + [DataMember(Name = "path")] + public string? Path { 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 = "owner")] + public UserProfile? Owner { get; set; } + + [DataMember(Name = "updater")] + public UserProfile? Updater { get; set; } + + /// + /// 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; } + + /// + /// Indicates if the content is configured as an element + /// + [DataMember(Name = "isElement")] + public bool IsElement { get; set; } + + /// + /// Property indicating if this item is part of a list view parent + /// + [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; } + + [DataMember(Name = "contentTypeId")] + public int? ContentTypeId { get; set; } + + [DataMember(Name = "contentTypeKey")] + public Guid ContentTypeKey { get; set; } + + [DataMember(Name = "contentTypeAlias", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string ContentTypeAlias { get; set; } = null!; + + [DataMember(Name = "sortOrder")] + public int SortOrder { 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 = "template")] + public string? TemplateAlias { get; set; } + + [DataMember(Name = "templateId")] + public int TemplateId { get; set; } + + [DataMember(Name = "allowedTemplates")] + public IDictionary? AllowedTemplates { get; set; } + + [DataMember(Name = "documentType")] + public ContentTypeBasic? DocumentType { get; set; } + + [DataMember(Name = "urls")] + public UrlInfo[]? Urls { 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 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 = "isBlueprint")] + public bool IsBlueprint { 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; } + + /// + /// 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 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/ContentItemDisplayBase.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplayBase.cs index a06fa62b1a8f..1adf69371b54 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(); - } + Notifications = new List(); + Errors = new Dictionary(); + } - /// - /// The name of the content type - /// - [DataMember(Name = "contentTypeName")] - public string? ContentTypeName { get; set; } + /// + /// 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; } + /// + /// 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; } - /// - /// 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 fed33c52b08d..400436421bf3 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; } + UploadedFiles = new List(); + Variants = new List(); + } - [DataMember(Name = "parentId", IsRequired = true)] - [Required] - public int ParentId { get; set; } + [DataMember(Name = "id", IsRequired = true)] + [Required] + public int Id { get; set; } - [DataMember(Name = "variants", IsRequired = true)] - public IEnumerable Variants { get; set; } + [DataMember(Name = "parentId", IsRequired = true)] + [Required] + public int ParentId { get; set; } - [DataMember(Name = "contentTypeAlias", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string ContentTypeAlias { get; set; } = null!; + [DataMember(Name = "variants", IsRequired = true)] + public IEnumerable Variants { get; set; } - /// - /// The template alias to save - /// - [DataMember(Name = "templateAlias")] - public string? TemplateAlias { get; set; } + [DataMember(Name = "contentTypeAlias", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string ContentTypeAlias { get; set; } = null!; - #region IContentSave + /// + /// The template alias to save + /// + [DataMember(Name = "templateAlias")] + public string? TemplateAlias { get; set; } - [DataMember(Name = "action", IsRequired = true)] - [Required] - public ContentSaveAction Action { get; set; } + #region IContentSave - [IgnoreDataMember] - public List UploadedFiles { get; } + [DataMember(Name = "action", IsRequired = true)] + [Required] + public ContentSaveAction Action { get; set; } - //These need explicit implementation because we are using internal models - /// - [IgnoreDataMember] - IContent IContentSave.PersistedContent { get; set; } = null!; + [IgnoreDataMember] + public List UploadedFiles { get; } - //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; - } + // These need explicit implementation because we are using internal models - #endregion + /// + [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 ee5a4600d4df..c4a3d7791b5c 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 3c772c08663f..35423f19a875 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 b31caaa901eb..ca8c2f1fc2af 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(); - } + Config = new Dictionary(); + Validation = new PropertyTypeValidation(); + } - [DataMember(Name = "label", IsRequired = true)] - [Required] - public string? Label { get; set; } + [DataMember(Name = "label", IsRequired = true)] + [Required] + public string? Label { get; set; } - [DataMember(Name = "description")] - public string? Description { 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 = "view", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string? View { get; set; } - [DataMember(Name = "config")] - public IDictionary? Config { get; set; } + [DataMember(Name = "config")] + public IDictionary? Config { get; set; } - [DataMember(Name = "hideLabel")] - public bool HideLabel { get; set; } + [DataMember(Name = "hideLabel")] + public bool HideLabel { get; set; } - [DataMember(Name = "labelOnTop")] - public bool? LabelOnTop { get; set; } + [DataMember(Name = "labelOnTop")] + public bool? LabelOnTop { get; set; } - [DataMember(Name = "validation")] - public PropertyTypeValidation Validation { get; set; } + [DataMember(Name = "validation")] + public PropertyTypeValidation Validation { get; set; } - [DataMember(Name = "readonly")] - public bool Readonly { 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 d40a25805e9c..b0045bb0381c 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 99ea69b364d1..62bf29ce82c8 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 3beb9705644d..889b03db6d67 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 00fce177b4b5..163514193485 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 80c24b8cc0e9..17d751760d7a 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 f46d8dc8f802..90dd6ce5c903 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 { + public ContentTypeBasic() + { + Blueprints = new Dictionary(); + Alias = string.Empty; + } + /// - /// A basic version of a content type + /// Overridden to apply our own validation attributes since this is not always required for other classes /// - /// - /// Generally used to return the minimal amount of data about a content type - /// - [DataContract(Name = "contentType", Namespace = "")] - public class ContentTypeBasic : EntityBasic - { - public ContentTypeBasic() - { - Blueprints = new Dictionary(); - Alias = string.Empty; - } + [Required] + [RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")] + [DataMember(Name = "alias")] + public override string Alias { get; set; } - /// - /// 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 + [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 { - get + if (Icon.IsNullOrWhiteSpace()) { - 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; + 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 + /// + /// 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 { - get + if (Thumbnail.IsNullOrWhiteSpace()) { - 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; + 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; } + /// + /// 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 = "blueprints")] + [ReadOnly(true)] + public IDictionary Blueprints { get; set; } - [DataMember(Name = "isContainer")] - [ReadOnly(true)] - public bool IsContainer { get; set; } + [DataMember(Name = "isContainer")] + [ReadOnly(true)] + public bool IsContainer { get; set; } - [DataMember(Name = "isElement")] - [ReadOnly(true)] - public bool IsElement { 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 4eb8563a6e9e..030923d29134 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(); - } + // 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 + // name, alias, icon, thumb, desc, inherited from basic + [DataMember(Name = "listViewEditorName")] + [ReadOnly(true)] + public string? ListViewEditorName { get; set; } - [DataMember(Name = "listViewEditorName")] - [ReadOnly(true)] - public string? ListViewEditorName { get; set; } + // Allowed child types + [DataMember(Name = "allowedContentTypes")] + public IEnumerable? AllowedContentTypes { get; set; } - //Allowed child types - [DataMember(Name = "allowedContentTypes")] - public IEnumerable? AllowedContentTypes { get; set; } + // Compositions + [DataMember(Name = "compositeContentTypes")] + public IEnumerable CompositeContentTypes { get; set; } - //Compositions - [DataMember(Name = "compositeContentTypes")] - public IEnumerable CompositeContentTypes { get; set; } + // Locked compositions + [DataMember(Name = "lockedCompositeContentTypes")] + public IEnumerable? LockedCompositeContentTypes { get; set; } - //Locked compositions - [DataMember(Name = "lockedCompositeContentTypes")] - public IEnumerable? LockedCompositeContentTypes { get; set; } + [DataMember(Name = "allowAsRoot")] + public bool AllowAsRoot { 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; } + /// + /// 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; } - } +[DataContract(Name = "contentType", Namespace = "")] +public abstract class ContentTypeCompositionDisplay : ContentTypeCompositionDisplay + where TPropertyTypeDisplay : PropertyTypeDisplay +{ + protected ContentTypeCompositionDisplay() => - [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>(); - } + // initialize collections so at least their never null + Groups = new List>(); - //Tabs - [DataMember(Name = "groups")] - public IEnumerable> Groups { get; set; } - } + // 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 55c4a07cfd0d..d6ad7c7ba227 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(); - } + AllowedContentTypes = new List(); + CompositeContentTypes = new List(); + } - //Compositions - [DataMember(Name = "compositeContentTypes")] - public IEnumerable CompositeContentTypes { get; set; } + // Compositions + [DataMember(Name = "compositeContentTypes")] + public IEnumerable CompositeContentTypes { get; set; } - [DataMember(Name = "allowAsRoot")] - public bool AllowAsRoot { get; set; } + [DataMember(Name = "allowAsRoot")] + public bool AllowAsRoot { get; set; } - //Allowed child types - [DataMember(Name = "allowedContentTypes")] - public IEnumerable AllowedContentTypes { get; set; } + // Allowed child types + [DataMember(Name = "allowedContentTypes")] + public IEnumerable AllowedContentTypes { get; set; } - [DataMember(Name = "historyCleanup")] - public HistoryCleanupViewModel? HistoryCleanup { get; set; } + [DataMember(Name = "historyCleanup")] + public HistoryCleanupViewModel? HistoryCleanup { get; set; } - /// - /// Custom validation - /// - /// - /// - public virtual IEnumerable Validate(ValidationContext validationContext) + /// + /// Custom validation + /// + /// + /// + public virtual IEnumerable Validate(ValidationContext validationContext) + { + if (CompositeContentTypes.Any(x => x.IsNullOrWhiteSpace())) { - if (CompositeContentTypes.Any(x => x.IsNullOrWhiteSpace())) - yield return new ValidationResult("Composite Content Type value cannot be null", new[] { "CompositeContentTypes" }); + 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; } /// - /// Abstract model used to save content types + /// Custom validation /// - /// - [DataContract(Name = "contentType", Namespace = "")] - public abstract class ContentTypeSave : ContentTypeSave - where TPropertyType : PropertyTypeBasic + /// + /// + public override IEnumerable Validate(ValidationContext validationContext) { - protected ContentTypeSave() + foreach (ValidationResult validationResult in base.Validate(validationContext)) { - Groups = new List>(); + yield return validationResult; } - /// - /// 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 (IGrouping> duplicateGroupAlias in Groups + .GroupBy(x => x.Alias).Where(x => x.Count() > 1)) { - foreach (var validationResult in base.Validate(validationContext)) + var lastGroupIndex = Groups.IndexOf(duplicateGroupAlias.Last()); + yield return new ValidationResult("Duplicate aliases are not allowed: " + duplicateGroupAlias.Key, new[] { - yield return validationResult; - } + // TODO: We don't display the alias yet, so add the validation message to the name + $"Groups[{lastGroupIndex}].Name", + }); + } - 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" - }); - } + 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 57b1c98d543b..476e772743a6 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 0a2bea7f8856..2b728c04da79 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 f7ea69f7ce88..ed9568590f08 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 44f0b31c2552..15b97ab24f2e 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 { - /// - /// Represents the variant info for a content item - /// - [DataContract(Name = "contentVariant", Namespace = "")] - public class ContentVariantDisplay : ITabbedContent, IContentProperties, INotificationModel + public ContentVariantDisplay() { - public ContentVariantDisplay() - { - Tabs = new List>(); - Notifications = new List(); - } + Tabs = new List>(); + Notifications = new List(); + } - [DataMember(Name = "name", IsRequired = true)] - public string? Name { get; set; } + [DataMember(Name = "name", IsRequired = true)] + public string? Name { get; set; } - [DataMember(Name = "displayName")] - public string? DisplayName { get; set; } + [DataMember(Name = "displayName")] + public string? DisplayName { get; set; } - /// - /// Defines the tabs containing display properties - /// - [DataMember(Name = "tabs")] - public IEnumerable> Tabs { 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; } - /// - /// 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 = "segment")] + public string? Segment { 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 = "state")] + public ContentSavedState State { get; set; } - [DataMember(Name = "segment")] - public string? Segment { get; set; } + [DataMember(Name = "updateDate")] + public DateTime UpdateDate { get; set; } - [DataMember(Name = "state")] - public ContentSavedState State { get; set; } + [DataMember(Name = "createDate")] + public DateTime CreateDate { get; set; } - [DataMember(Name = "updateDate")] - public DateTime UpdateDate { get; set; } + [DataMember(Name = "publishDate")] + public DateTime? PublishDate { get; set; } - [DataMember(Name = "createDate")] - public DateTime CreateDate { 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 = "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; } - /// - /// 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; } - } + /// + /// 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; } +public class ContentVariantScheduleDisplay : ContentVariantDisplay +{ + [DataMember(Name = "releaseDate")] + public DateTime? ReleaseDate { get; set; } - [DataMember(Name = "expireDate")] - public DateTime? ExpireDate { 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 b1db2759f0e1..a6f99ab58636 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 153f495a70f3..7bb0d427eadc 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 97a217716784..a324bb4bcea2 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 a82a6eb257c2..514f9b86187b 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 cbe5552b1e94..7f3c93d12653 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(); - } - - /// - /// The alias of the property editor - /// - [DataMember(Name = "selectedEditor", IsRequired = true)] - [Required] - public string? SelectedEditor { get; set; } + [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 c8699472d5da..47711fc0a30a 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 = "documentTypes")] + public IEnumerable DocumentTypes { get; set; } = Enumerable.Empty(); - [DataMember(Name = "mediaTypes")] - public IEnumerable MediaTypes { get; set; } = Enumerable.Empty(); + [DataMember(Name = "mediaTypes")] + public IEnumerable MediaTypes { get; set; } = Enumerable.Empty(); - [DataMember(Name = "memberTypes")] - public IEnumerable MemberTypes { get; set; } = Enumerable.Empty(); + [DataMember(Name = "memberTypes")] + public IEnumerable MemberTypes { get; set; } = Enumerable.Empty(); - [DataContract(Name = "contentType", Namespace = "")] - public class ContentTypeReferences : EntityBasic - { - [DataMember(Name = "properties")] - public object? Properties { get; set; } + [DataContract(Name = "contentType", Namespace = "")] + public class ContentTypeReferences : EntityBasic + { + [DataMember(Name = "properties")] + public object? Properties { get; set; } - [DataContract(Name = "property", Namespace = "")] - public class PropertyTypeReferences - { - [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 3795e42782e8..8968fb0795e2 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 d8cfaf110462..59e5bffb4b4d 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(); - } + 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 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; } - /// - /// 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; } - /// - /// 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 adf279c4121e..15aab0c7ef62 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 00d8b339f9be..9e534820fa4a 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 0e652e7160be..85585c45ba16 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 4ad4002b7713..afd36b6acc84 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 aa42abbf5622..cf58bcb2ec5a 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 6f56c9229270..3c292a7e6a73 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() => + + // initialize collections so at least their never null + AllowedTemplates = new List(); - //name, alias, icon, thumb, desc, inherited from the content type + // name, alias, icon, thumb, desc, inherited from the content type - // Templates - [DataMember(Name = "allowedTemplates")] - public IEnumerable AllowedTemplates { get; set; } + // Templates + [DataMember(Name = "allowedTemplates")] + public IEnumerable AllowedTemplates { get; set; } - [DataMember(Name = "defaultTemplate")] - public EntityBasic? DefaultTemplate { get; set; } + [DataMember(Name = "defaultTemplate")] + public EntityBasic? DefaultTemplate { 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 = "apps")] - public IEnumerable? ContentApps { get; set; } + [DataMember(Name = "apps")] + public IEnumerable? ContentApps { get; set; } - [DataMember(Name = "historyCleanup")] - public HistoryCleanupViewModel? HistoryCleanup { 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 2e509ea5db59..af13e88f9b14 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 - { - /// - /// The list of allowed templates to assign (template alias) - /// - [DataMember(Name = "allowedTemplates")] - public IEnumerable? AllowedTemplates { get; set; } + [DataMember(Name = "allowedTemplates")] + public IEnumerable? AllowedTemplates { get; set; } - /// - /// The default template to assign (template alias) - /// - [DataMember(Name = "defaultTemplate")] - public string? DefaultTemplate { 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) + /// + /// 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 573909a6105f..7a6a58443858 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; - } + Name = name; + Lang = lang; + } - [DataMember(Name = "name")] - public string Name { get; } + [DataMember(Name = "name")] + public string Name { get; } - [DataMember(Name = "lang")] - public int Lang { get; } + [DataMember(Name = "lang")] + public int Lang { get; } - [DataMember(Name = "duplicate")] - public bool Duplicate { get; set; } + [DataMember(Name = "duplicate")] + public bool Duplicate { get; set; } - [DataMember(Name = "other")] - public string? Other { 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 a91e740e795b..391616b8dcd5 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 6c8c1b50e35d..0920e45f2970 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 772da930e9d3..36a837e8e88c 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; - } + 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 = "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 = "id", IsRequired = true)] + [Required] + public object? Id { get; set; } - [DataMember(Name = "udi")] - [ReadOnly(true)] - public Udi? Udi { get; set; } + [DataMember(Name = "udi")] + [ReadOnly(true)] + public Udi? Udi { get; set; } - [DataMember(Name = "icon")] - public string? Icon { get; set; } + [DataMember(Name = "icon")] + public string? Icon { get; set; } - [DataMember(Name = "trashed")] - [ReadOnly(true)] - public bool Trashed { 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; } + /// + /// 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; } + [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; } + /// + /// 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; } - } + /// + /// 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 ff77e3aeb5ce..f345d881b6ba 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; + _results = results; + TotalResults = totalFound; + } - public EntitySearchResults(IEnumerable results, long totalFound) - { - _results = results; - TotalResults = totalFound; - } + [DataMember(Name = "totalResults")] + public long TotalResults { get; set; } - [DataMember(Name = "totalResults")] - public long TotalResults { get; set; } + public IEnumerator GetEnumerator() => _results.GetEnumerator(); - public IEnumerator GetEnumerator() => _results.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_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 d73687c03901..c3f49d5b7d1c 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 a0d9bbbcb3a1..386ca5f12f9d 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 bool _preventCleanup; - private int? _keepAllVersionsNewerThanDays; - private int? _keepLatestVersionPerDayForDays; + private int? _keepAllVersionsNewerThanDays; + private int? _keepLatestVersionPerDayForDays; + private bool _preventCleanup; - [DataMember(Name = "preventCleanup")] - public bool PreventCleanup - { - 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 303ff4eda375..1860dc8feb3d 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 -{ - [DataContract(Name = "historyCleanup", Namespace = "")] - public class HistoryCleanupViewModel : HistoryCleanup - { +namespace Umbraco.Cms.Core.Models.ContentEditing; - [DataMember(Name = "globalEnableCleanup")] - public bool GlobalEnableCleanup { get; set; } +[DataContract(Name = "historyCleanup", Namespace = "")] +public class HistoryCleanupViewModel : HistoryCleanup +{ + [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 fc263a3b9179..e0216f66f867 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 ca8b2439c26d..3520c078b19d 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 dfaf18347921..effccf95faa6 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 4352771cacfd..9607146eda43 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 a1d4198427d0..7e467ff124c9 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 ac104c0e1b5b..15b75a82cf82 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 3f1d84715139..13f7375c3d5a 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 0a0ed03a2a16..15e63eabedc7 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 551065c56699..9b7bde570d70 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 729a086864c8..1add8da7d851 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 f794143aaba2..9919004a5036 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(); - } + 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 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 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 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 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 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 view. - /// - [DataMember(Name = "view")] - public string View { get; set; } = null!; + /// + /// Gets or sets the parameters. + /// + [DataMember(Name = "parameters")] + public IEnumerable Parameters { get; set; } - /// - /// 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 233a58cd08a6..3db1cd5820c5 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 { - /// - /// 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 = "alias", IsRequired = true)] + [Required] + public string Alias { get; set; } = null!; - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "sortOrder")] - public int SortOrder { 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 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; } + /// + /// 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 8cd630d66f73..3a532fcc12d4 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 a56911f70725..784e5510fb58 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 06c201ab6760..7bac43b25dcd 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 2c7c50550d31..899be9504057 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 1ef2a1988b5a..b3fdeea1e251 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 d148d889210c..7ef1ce5f729b 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 = "username")] + public string? Username { get; set; } - [DataMember(Name = "email")] - public string? Email { get; set; } + [DataMember(Name = "email")] + public string? Email { get; set; } - [DataMember(Name = "properties")] - public override IEnumerable Properties - { - get => base.Properties; - set => base.Properties = value; - } + [DataMember(Name = "properties")] + public override IEnumerable Properties + { + 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 5448c40b1e36..161c085d355e 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(); - } - - [DataMember(Name = "contentType")] - public ContentTypeBasic? ContentType { get; set; } - - [DataMember(Name = "username")] - public string? Username { get; set; } - - [DataMember(Name = "email")] - public string? Email { get; set; } - - [DataMember(Name = "isLockedOut")] - public bool IsLockedOut { get; set; } - - [DataMember(Name = "isApproved")] - public bool IsApproved { get; set; } - - //[DataMember(Name = "membershipScenario")] - //public MembershipScenario MembershipScenario { 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; } - } + public MemberDisplay() => + + // MemberProviderFieldMapping = new Dictionary(); + ContentApps = new List(); + + [DataMember(Name = "contentType")] + public ContentTypeBasic? ContentType { get; set; } + + [DataMember(Name = "username")] + public string? Username { get; set; } + + [DataMember(Name = "email")] + public string? Email { get; set; } + + [DataMember(Name = "isLockedOut")] + public bool IsLockedOut { get; set; } + + [DataMember(Name = "isApproved")] + public bool IsApproved { get; set; } + + // [DataMember(Name = "membershipScenario")] + // public MembershipScenario MembershipScenario { 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; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberGroupDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MemberGroupDisplay.cs index 2d930727aa99..0804fd53d74e 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 +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "memberGroup", Namespace = "")] +public class MemberGroupDisplay : EntityBasic, INotificationModel { - [DataContract(Name = "memberGroup", Namespace = "")] - public class MemberGroupDisplay : EntityBasic, INotificationModel - { - public MemberGroupDisplay() - { - Notifications = new List(); - } + 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; } - } + /// + /// 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 2b863a758d64..292d410625d7 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 c4a5382e84fa..cd89f46fc663 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 b25f2ae5c8a9..9ef0ebf3b987 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 873883c8db8c..1038440974e8 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 903c87341a45..2963618e1b01 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 -{ - /// - public class MemberSave : ContentBaseSave - { +namespace Umbraco.Cms.Core.Models.ContentEditing; - [DataMember(Name = "username", IsRequired = true)] - [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] - public string Username { get; set; } = null!; +/// +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 = "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 = "password")] + public ChangingPasswordModel? Password { get; set; } - [DataMember(Name = "memberGroups")] - public IEnumerable? Groups { 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); + /// + /// 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 = "isLockedOut")] + public bool IsLockedOut { get; set; } - [DataMember(Name = "isApproved")] - public bool IsApproved { get; set; } + [DataMember(Name = "isApproved")] + public bool IsApproved { get; set; } - private T? GetPropertyValue(string alias) + 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 67e390f37868..ea8aa5c1e365 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 80ac46ae099b..59a604749461 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 5a93ae94c906..5a6511134570 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 = type }); + } - model.Notifications?.Add(new BackOfficeNotification() - { - Header = header, - Message = msg, - NotificationType = NotificationStyle.Success - }); + public static void AddSuccessNotification(this INotificationModel model, string header, string msg) + { + if (model.Exists(header, msg, NotificationStyle.Success)) + { + return; } - public static void AddErrorNotification(this INotificationModel model, string? header, string msg) + model.Notifications?.Add(new BackOfficeNotification { - if (model.Exists(header, msg, NotificationStyle.Error)) return; + Header = header, + Message = msg, + NotificationType = NotificationStyle.Success, + }); + } - model.Notifications?.Add(new BackOfficeNotification() - { - Header = header, - Message = msg, - NotificationType = NotificationStyle.Error - }); + public static void AddErrorNotification(this INotificationModel model, string? header, string msg) + { + if (model.Exists(header, msg, NotificationStyle.Error)) + { + return; } - public static void AddWarningNotification(this INotificationModel model, string header, string msg) + model.Notifications?.Add(new BackOfficeNotification { - if (model.Exists(header, msg, NotificationStyle.Warning)) return; + Header = header, + Message = msg, + NotificationType = NotificationStyle.Error, + }); + } - model.Notifications?.Add(new BackOfficeNotification() - { - Header = header, - Message = msg, - NotificationType = NotificationStyle.Warning - }); + public static void AddWarningNotification(this INotificationModel model, string header, string msg) + { + if (model.Exists(header, msg, NotificationStyle.Warning)) + { + return; } - public static void AddInfoNotification(this INotificationModel model, string header, string msg) + model.Notifications?.Add(new BackOfficeNotification { - if (model.Exists(header, msg, NotificationStyle.Info)) return; + Header = header, + Message = msg, + NotificationType = NotificationStyle.Warning, + }); + } - model.Notifications?.Add(new BackOfficeNotification() - { - Header = header, - Message = msg, - NotificationType = NotificationStyle.Info - }); + public static void AddInfoNotification(this INotificationModel model, string header, string msg) + { + if (model.Exists(header, msg, NotificationStyle.Info)) + { + return; } - 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 = 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 d79be81725e5..56275bfb6cbe 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(); - } + Value = value; + Notifications = new List(); + } - /// - /// The generic value - /// - [DataMember(Name = "value")] - public T Value { get; private set; } + /// + /// The generic value + /// + [DataMember(Name = "value")] + public T Value { get; private set; } - /// - /// The notifications - /// - [DataMember(Name = "notifications")] - public List Notifications { 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 c27cf70ccfe5..ecbcc027f47b 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; } - - /// - /// The id of the node to move or copy - /// - [DataMember(Name = "id", IsRequired = true)] - [Required] - public int Id { get; set; } + [DataMember(Name = "parentId", IsRequired = true)] + [Required] + public int ParentId { 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; } + /// + /// 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 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 a8c17d1850a5..1fe9e9b525eb 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 ee4029cab3fb..603ec953b078 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 4682b752b9f1..c2f69218b3b0 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 +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "objectType", Namespace = "")] +public class ObjectType { - [DataContract(Name = "objectType", Namespace = "")] - public class ObjectType - { - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "id")] - public Guid Id { 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 c6e446fc39ba..9bdb664579f7 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 = "description")] - public string? Description { get; set; } - - [DataMember(Name = "checked")] - public bool Checked { 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!; - - /// - /// The letter from the IAction - /// - [DataMember(Name = "permissionCode")] - public string? PermissionCode { get; set; } - - public object Clone() - { - return this.MemberwiseClone(); - } - } + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "description")] + public string? Description { get; set; } + + [DataMember(Name = "checked")] + public bool Checked { 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!; + + /// + /// The letter from the IAction + /// + [DataMember(Name = "permissionCode")] + public string? PermissionCode { get; set; } + + public object Clone() => MemberwiseClone(); } diff --git a/src/Umbraco.Core/Models/ContentEditing/PostedFiles.cs b/src/Umbraco.Core/Models/ContentEditing/PostedFiles.cs index 69029c961a33..5d713691413c 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 +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 { - /// - /// 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() { - public PostedFiles() - { - UploadedFiles = new List(); - Notifications = new List(); - } - public List UploadedFiles { get; private set; } - - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } + 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 56ca1c19071d..79769559db24 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 +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Used to create a folder with the MediaController +/// +[DataContract] +public class PostedFolder { - /// - /// Used to create a folder with the MediaController - /// - [DataContract] - public class PostedFolder - { - [DataMember(Name = "parentId")] - public string? ParentId { get; set; } + [DataMember(Name = "parentId")] + public string? ParentId { get; set; } - [DataMember(Name = "name")] - public string? Name { 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 b73f2897e7d4..498537cf1e5d 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 0431fb270fed..5b45776a8e4a 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 -{ - [DataContract(Name = "propertyGroup", Namespace = "")] - public abstract class PropertyGroupBasic - { - /// - /// 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 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; } - - [DataMember(Name = "key")] - public Guid Key { get; set; } - - [DataMember(Name = "type")] - public PropertyGroupType Type { get; set; } +namespace Umbraco.Cms.Core.Models.ContentEditing; - [Required] - [DataMember(Name = "name")] - public string? Name { get; set; } - - [Required] - [DataMember(Name = "alias")] - public string Alias { get; set; } = null!; - - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } - } +[DataContract(Name = "propertyGroup", Namespace = "")] +public abstract class PropertyGroupBasic +{ + /// + /// 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 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; } + + [DataMember(Name = "key")] + public Guid Key { get; set; } + + [DataMember(Name = "type")] + public PropertyGroupType Type { get; set; } + + [Required] + [DataMember(Name = "name")] + public string? Name { get; set; } + + [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(); - } +[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 = "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 6f1317f3eb86..4e3b530f99d4 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 a543d8534766..67a200cf65ee 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(); - } + 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 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 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; } - } + /// + /// 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 0aded31a1861..4574e62cde4a 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; } - - // 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!; - - [DataMember(Name = "description")] - public string? Description { get; set; } - - [DataMember(Name = "validation")] - public PropertyTypeValidation? Validation { get; set; } - - [DataMember(Name = "label")] - [Required] - public string Label { get; set; } = null!; - - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } - - [DataMember(Name = "dataTypeId")] - [Required] - public int DataTypeId { get; set; } - - [DataMember(Name = "dataTypeKey")] - [ReadOnly(true)] - public Guid DataTypeKey { get; set; } - - [DataMember(Name = "dataTypeName")] - [ReadOnly(true)] - public string? DataTypeName { 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; } - - [DataMember(Name = "allowCultureVariant")] - public bool AllowCultureVariant { get; set; } - - [DataMember(Name = "allowSegmentVariant")] - public bool AllowSegmentVariant { get; set; } - - [DataMember(Name = "labelOnTop")] - public bool LabelOnTop { 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; } + + [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 = "validation")] + public PropertyTypeValidation? Validation { get; set; } + + [DataMember(Name = "label")] + [Required] + public string Label { get; set; } = null!; + + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } + + [DataMember(Name = "dataTypeId")] + [Required] + public int DataTypeId { get; set; } + + [DataMember(Name = "dataTypeKey")] + [ReadOnly(true)] + public Guid DataTypeKey { get; set; } + + [DataMember(Name = "dataTypeName")] + [ReadOnly(true)] + public string? DataTypeName { 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; } + + [DataMember(Name = "allowCultureVariant")] + public bool AllowCultureVariant { get; set; } + + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { 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 5ca3e4de5c28..926ea50106fa 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 -{ - [DataContract(Name = "propertyType")] - public class PropertyTypeDisplay : PropertyTypeBasic - { - [DataMember(Name = "editor")] - [ReadOnly(true)] - public string? Editor { get; set; } +namespace Umbraco.Cms.Core.Models.ContentEditing; - [DataMember(Name = "view")] - [ReadOnly(true)] - public string? View { get; set; } +[DataContract(Name = "propertyType")] +public class PropertyTypeDisplay : PropertyTypeBasic +{ + [DataMember(Name = "editor")] + [ReadOnly(true)] + public string? Editor { get; set; } - [DataMember(Name = "config")] - [ReadOnly(true)] - public IDictionary? Config { get; set; } + [DataMember(Name = "view")] + [ReadOnly(true)] + public string? View { 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; } + [DataMember(Name = "config")] + [ReadOnly(true)] + public IDictionary? Config { 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; } + /// + /// 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 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 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; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeValidation.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeValidation.cs index 5db1ab813919..76e9547c0769 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 199ca34ceb3e..1c21aec0339e 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 e4b026b6eb36..8a1a8d91c924 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 0decb18414a9..d4cb9602517f 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 906fdf3a40e2..b6168e13d501 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 -{ - [DataContract(Name = "relationType", Namespace = "")] - public class RelationTypeDisplay : EntityBasic, INotificationModel - { - public RelationTypeDisplay() - { - Notifications = new List(); - } - - [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 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; } +namespace Umbraco.Cms.Core.Models.ContentEditing; - /// - /// 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; } - - /// - /// 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; } - } +[DataContract(Name = "relationType", Namespace = "")] +public class RelationTypeDisplay : EntityBasic, INotificationModel +{ + public RelationTypeDisplay() => Notifications = new List(); + + [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 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 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 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 f541158095d9..910d2827f7d4 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 06fcc5d12415..782f34c88ced 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; } + Insert, + Selection, + All, +} - [DataMember(Name = "alias")] - public string? Alias { get; set; } +[DataContract(Name = "richtexteditorcommand", Namespace = "")] +public class RichTextEditorCommand +{ + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "mode")] - public RichTextEditorCommandMode Mode { get; set; } - } + [DataMember(Name = "alias")] + public string? Alias { get; set; } - public enum RichTextEditorCommandMode - { - Insert, - Selection, - All - } + [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 e80b25f4aecd..c621aa8c59ea 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 3740f47fc648..c35eb1e18c95 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 ca0e3ff9affe..dfd4511aa168 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 53facfe990cf..8a7fc5360597 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 e2fc1ff2d7f7..f86ffc232a95 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 2d550a4457ca..fb7b0fc1012c 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 558d73b49bec..68d34822c389 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 { - /// - /// 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 = "name")] + public string Name { get; set; } = null!; - [DataMember(Name = "alias")] - public string Alias { 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; } - } + /// + /// 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; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/SimpleNotificationModel.cs b/src/Umbraco.Core/Models/ContentEditing/SimpleNotificationModel.cs index e6db2b933a19..9fe429cf3fba 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 39e2027b274d..48b3d71cacc1 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 11d3b814c16f..6a8d7c14fe70 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 c5f827300a7c..f7af3d984fef 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 4bcd824670da..ab1e92d340e8 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 { - /// - /// Represents a tab in the UI - /// - [DataContract(Name = "tab", Namespace = "")] - public class Tab - { - [DataMember(Name = "id")] - public int Id { get; set; } + [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 string? Type { get; set; } + [DataMember(Name = "type")] + public string? Type { get; set; } - [DataMember(Name = "active")] - public bool IsActive { get; set; } + [DataMember(Name = "active")] + public bool IsActive { get; set; } - [DataMember(Name = "label")] - public string? Label { get; set; } + [DataMember(Name = "label")] + public string? Label { get; set; } - [DataMember(Name = "alias")] - public string? Alias { 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; + /// + /// 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 afc64e7fafbe..c47424cdf0f9 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 -{ - public abstract class TabbedContentItem : ContentItemBasic, ITabbedContent where T : ContentPropertyBasic - { - protected TabbedContentItem() - { - Tabs = new List>(); - } +namespace Umbraco.Cms.Core.Models.ContentEditing; - /// - /// Defines the tabs containing display properties - /// - [DataMember(Name = "tabs")] - public IEnumerable> Tabs { get; set; } +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 - { - get => Tabs.Where(x => x.Properties is not null).SelectMany(x => x.Properties!); - set => throw new NotImplementedException(); - } + /// + /// 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(); } + + /// + /// 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 fd67d5593659..b6dadcdc2a34 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 -{ - [DataContract(Name = "template", Namespace = "")] - public class TemplateDisplay : INotificationModel - { +namespace Umbraco.Cms.Core.Models.ContentEditing; - [DataMember(Name = "id")] - public int Id { get; set; } +[DataContract(Name = "template", Namespace = "")] +public class TemplateDisplay : INotificationModel +{ + [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 99533facc87b..f1b3dea9b261 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 { - /// - /// Represents a search result by entity type - /// - [DataContract(Name = "searchResult", Namespace = "")] - public class TreeSearchResult - { - [DataMember(Name = "appAlias")] - public string? AppAlias { get; set; } + [DataMember(Name = "appAlias")] + public string? AppAlias { get; set; } - [DataMember(Name = "treeAlias")] - public string? TreeAlias { get; set; } + [DataMember(Name = "treeAlias")] + public string? TreeAlias { 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 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; } + /// + /// 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 c77500c531b7..e089093174c0 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 - /// - public enum UmbracoEntityTypes - { - /// - /// Language - /// - Language, - - /// - /// User - /// - User, - - /// - /// Macro - /// - Macro, - - /// - /// Document - /// - Document, - - /// - /// Media - /// - Media, - - /// - /// Member Type - /// - MemberType, - - /// - /// Template - /// - Template, - - /// - /// Member Group - /// - MemberGroup, - - /// - /// "Media Type - /// - MediaType, - - /// - /// Document Type - /// - DocumentType, - - /// - /// Stylesheet - /// - Stylesheet, - - /// - /// Script - /// - Script, - - /// - /// Partial View - /// - PartialView, - - /// - /// Member - /// - Member, - - /// - /// Data Type - /// - DataType, - - /// - /// Property Type - /// - PropertyType, - - /// - /// Property Group - /// - PropertyGroup, - - /// - /// Dictionary Item - /// - DictionaryItem - } + /// Language + /// + Language, + + /// + /// User + /// + User, + + /// + /// Macro + /// + Macro, + + /// + /// Document + /// + Document, + + /// + /// Media + /// + Media, + + /// + /// Member Type + /// + MemberType, + + /// + /// Template + /// + Template, + + /// + /// Member Group + /// + MemberGroup, + + /// + /// "Media Type + /// + MediaType, + + /// + /// Document Type + /// + DocumentType, + + /// + /// Stylesheet + /// + Stylesheet, + + /// + /// Script + /// + Script, + + /// + /// Partial View + /// + PartialView, + + /// + /// Member + /// + Member, + + /// + /// Data Type + /// + DataType, + + /// + /// Property Type + /// + PropertyType, + + /// + /// Property Group + /// + PropertyGroup, + + /// + /// Dictionary Item + /// + DictionaryItem, } diff --git a/src/Umbraco.Core/Models/ContentEditing/UnpublishContent.cs b/src/Umbraco.Core/Models/ContentEditing/UnpublishContent.cs index 7a4e6d28d8fa..cc77bf5dbfa1 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 0e8c711e833d..1a732ed01762 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; - } + Url = url; + AnchorValues = anchorValues; + } - [DataMember(Name = "url")] - public string Url { get; } + [DataMember(Name = "url")] + public string Url { get; } - [DataMember(Name = "anchorValues")] - public IEnumerable AnchorValues { 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 b2dc4ceb4a0a..6d20e54bfa0e 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(); - } + Notifications = new List(); + UserGroups = new List(); + } - [DataMember(Name = "username")] - public string? Username { get; set; } + [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; } + /// + /// 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; } + [DataMember(Name = "lastLoginDate")] + public DateTime? LastLoginDate { get; set; } - /// - /// Returns a list of different size avatars - /// - [DataMember(Name = "avatars")] - public string[]? Avatars { 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 = "userState")] + public UserState UserState { get; set; } - [DataMember(Name = "culture", IsRequired = true)] - public string? Culture { get; set; } + [DataMember(Name = "culture", IsRequired = true)] + public string? Culture { get; set; } - [DataMember(Name = "email", IsRequired = true)] - public string? Email { 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; } + /// + /// 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 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; } - } + /// + /// 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 01c2bcb70cec..88b31ee4a23c 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; } + + /// + /// The MD5 lowercase hash of the email which can be used by gravatar + /// + [DataMember(Name = "emailHash")] + public string? EmailHash { get; set; } + + [ReadOnly(true)] + [DataMember(Name = "userGroups")] + public string?[]? UserGroups { 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 = "startContentIds")] + public int[]? StartContentIds { 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; } + /// - /// Represents information for the current user + /// A list of sections the user is allowed to view. /// - [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; } - - /// - /// The MD5 lowercase hash of the email which can be used by gravatar - /// - [DataMember(Name = "emailHash")] - public string? EmailHash { get; set; } - - [ReadOnly(true)] - [DataMember(Name = "userGroups")] - public string?[]? UserGroups { 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 = "startContentIds")] - public int[]? StartContentIds { 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; } - } + [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 20e517cefca3..4b300c17a928 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(); - } + AvailableCultures = new Dictionary(); + StartContentIds = new List(); + StartMediaIds = new List(); + Navigation = new List(); + } - [DataMember(Name = "navigation")] - [ReadOnly(true)] - public IEnumerable Navigation { get; set; } + [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; } + /// + /// 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 = "startContentIds")] + public IEnumerable StartContentIds { get; set; } - [DataMember(Name = "startMediaIds")] - public IEnumerable StartMediaIds { 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; } + /// + /// 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 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; } + /// + /// 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 = "failedPasswordAttempts")] + [ReadOnly(true)] + public int FailedPasswordAttempts { get; set; } - [DataMember(Name = "lastLockoutDate")] - [ReadOnly(true)] - public DateTime? LastLockoutDate { get; set; } + [DataMember(Name = "lastLockoutDate")] + [ReadOnly(true)] + public DateTime? LastLockoutDate { get; set; } - [DataMember(Name = "lastPasswordChangeDate")] - [ReadOnly(true)] - public DateTime? LastPasswordChangeDate { get; set; } + [DataMember(Name = "lastPasswordChangeDate")] + [ReadOnly(true)] + public DateTime? LastPasswordChangeDate { get; set; } - [DataMember(Name = "createDate")] - [ReadOnly(true)] - public DateTime CreateDate { get; set; } + [DataMember(Name = "createDate")] + [ReadOnly(true)] + public DateTime CreateDate { get; set; } - [DataMember(Name = "updateDate")] - [ReadOnly(true)] - public DateTime UpdateDate { 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 ffcfde8368a4..036694f2b624 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 697a0a21004f..30cca62c4a44 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(); - } + Users = Enumerable.Empty(); + AssignedPermissions = Enumerable.Empty(); + } - [DataMember(Name = "users")] - public IEnumerable Users { get; set; } + [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 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; } - } + /// + /// 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 e782d6963561..1c04496e04b8 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 -{ - /// - /// Used to assign user group permissions to a content node - /// - [DataContract(Name = "contentPermission", Namespace = "")] - public class UserGroupPermissionsSave - { - public UserGroupPermissionsSave() - { - AssignedPermissions = new Dictionary>(); - } +namespace Umbraco.Cms.Core.Models.ContentEditing; - // 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! +/// +/// Used to assign user group permissions to a content node +/// +[DataContract(Name = "contentPermission", Namespace = "")] +public class UserGroupPermissionsSave +{ + public UserGroupPermissionsSave() => AssignedPermissions = new Dictionary>(); - [DataMember(Name = "contentId", IsRequired = true)] - [Required] - public int ContentId { get; set; } + // 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; } + /// + /// 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) + [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())) { - 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 1bf792381785..ef49a6ab2808 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; } + /// + /// 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 = "alias", IsRequired = true)] + [Required] + public override string Alias { get; set; } = string.Empty; - [DataMember(Name = "sections")] - public IEnumerable? Sections { get; set; } + [DataMember(Name = "sections")] + public IEnumerable? Sections { get; set; } - [DataMember(Name = "users")] - public IEnumerable? Users { get; set; } + [DataMember(Name = "users")] + public IEnumerable? Users { get; set; } - [DataMember(Name = "startContentId")] - public int? StartContentId { get; set; } + [DataMember(Name = "startContentId")] + public int? StartContentId { get; set; } - [DataMember(Name = "startMediaId")] - public int? StartMediaId { 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 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 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; } + /// + /// The real persisted user group + /// + [IgnoreDataMember] + public IUserGroup? PersistedUserGroup { get; set; } - public IEnumerable Validate(ValidationContext validationContext) + 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 7b3014369aa6..02a10b45afff 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 = "userGroups")] + [Required] + public IEnumerable UserGroups { get; set; } = null!; - [DataMember(Name = "email", IsRequired = true)] - [Required] - [EmailAddress] - public string Email { 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 = "username")] + public string? Username { get; set; } - [DataMember(Name = "message")] - public string? Message { get; set; } + [DataMember(Name = "message")] + public string? Message { get; set; } - public IEnumerable Validate(ValidationContext validationContext) + 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 9ade7735e7b1..441972e8bc15 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 -{ - /// - /// 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; } +namespace Umbraco.Cms.Core.Models.ContentEditing; - [DataMember(Name = "name", IsRequired = true)] - [Required] - public string? Name { get; set; } +/// +/// 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 = "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 6e03248a316b..e0a3d41d4fd6 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 = "changePassword", IsRequired = true)] + public ChangingPasswordModel? ChangePassword { get; set; } - [DataMember(Name = "id", IsRequired = true)] - [Required] - public new int Id { 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 = "username", IsRequired = true)] + [Required] + public string Username { get; set; } = null!; - [DataMember(Name = "culture", IsRequired = true)] - [Required] - public string Culture { 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 = "email", IsRequired = true)] + [Required] + [EmailAddress] + public string Email { get; set; } = null!; - [DataMember(Name = "userGroups")] - [Required] - public IEnumerable UserGroups { get; set; } = null!; + [DataMember(Name = "userGroups")] + [Required] + public IEnumerable UserGroups { get; set; } = null!; - [DataMember(Name = "startContentIds")] - public int[]? StartContentIds { get; set; } + [DataMember(Name = "startContentIds")] + public int[]? StartContentIds { get; set; } - [DataMember(Name = "startMediaIds")] - public int[]? StartMediaIds { get; set; } + [DataMember(Name = "startMediaIds")] + public int[]? StartMediaIds { get; set; } - public IEnumerable Validate(ValidationContext validationContext) + 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 cead39f01939..5d81ea367efd 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 ab882342b538..32889331e002 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 +namespace Umbraco.Cms.Core.Models; + +public class ContentModel : ContentModel + where TContent : IPublishedContent { - 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; + /// + /// 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; } - } + /// + /// 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 4ab39f1669e4..bf0879f8dd74 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) - { - foreach(var culture in content.EditedCultures.ToList()) - { - if (content.CultureInfos is null) - { - continue; - } + throw new ArgumentNullException(nameof(culture)); + } - if (!content.CultureInfos.TryGetValue(culture, out var editedInfos)) - { - continue; - } + if (string.IsNullOrWhiteSpace(culture)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(culture)); + } - // 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, name, date); + } - content.CultureInfos?.AddOrUpdate(culture, editedInfos?.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; + } - if (!publishing) - { - return; - } + content.CultureInfos?.AddOrUpdate(culture!, infos.Name, DateTime.Now); + } - foreach (var culture in content.PublishedCultures.ToList()) + /// + /// 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()) { - 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); - - if (content.CultureInfos?.TryGetValue(culture, out ContentCultureInfos infos) ?? false) - { - SetCultureInfo(content, culture, infos.Name, date); - } + content.CultureInfos?.AddOrUpdate(culture, editedInfos?.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 (!publishing) + { + return; + } + + foreach (var culture in content.PublishedCultures.ToList()) { - if (!content.Published || !content.ContentType.VariesByCulture() || !content.IsPropertyDirty("PublishCultureInfos")) - return Array.Empty(); + if (content.PublishCultureInfos is null) + { + continue; + } - var culturesUnpublishing = content.CultureInfos?.Values - .Where(x => content.IsPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + x.Culture)) - .Select(x => x.Culture); + 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; + } - return culturesUnpublishing?.ToList(); + content.PublishCultureInfos.AddOrUpdate(culture, publishInfos.Name, date); + + if (content.CultureInfos?.TryGetValue(culture, out ContentCultureInfos infos) ?? false) + { + SetCultureInfo(content, culture, infos.Name, date); + } } + } - /// - /// Copies values from another document. - /// - public static void CopyFrom(this IContent content, IContent other, string? culture = "*") + /// + /// 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")) { - if (other.ContentTypeId != content.ContentTypeId) - throw new InvalidOperationException("Cannot copy values from a different content type."); + return Array.Empty(); + } - culture = culture?.ToLowerInvariant().NullOrWhiteSpaceAsNull(); + IEnumerable? culturesUnpublishing = content.CultureInfos?.Values + .Where(x => content.IsPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + x.Culture)) + .Select(x => x.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(culture, "*", true)) - throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); + return culturesUnpublishing?.ToList(); + } - // copying from the same Id and VersionPk - var copyingFromSelf = content.Id == other.Id && content.VersionId == other.VersionId; - var published = copyingFromSelf; + /// + /// 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(); - // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails + // 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; - // clear all existing properties for the specified culture - foreach (var property in content.Properties) + // 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) { - // 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) && - (culture == "*" || (pvalue.Culture?.InvariantEquals(culture) ?? false))) - { - property.SetValue(null, pvalue.Culture, pvalue.Segment); - } + foreach (IPropertyValue pvalue in property.Values) + { + if ((property.PropertyType?.SupportsVariation(pvalue.Culture, pvalue.Segment, true) ?? false) && + (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) + // copy properties from 'other' + IPropertyCollection otherProperties = other.Properties; + foreach (IProperty otherProperty in otherProperties) + { + if (!otherProperty?.PropertyType?.SupportsVariation(culture, "*", true) ?? true) { - if (!otherProperty?.PropertyType?.SupportsVariation(culture, "*", wildcards: true) ?? true) - continue; + continue; + } - var alias = otherProperty?.PropertyType.Alias; - if (otherProperty is not null && alias is not null) + var alias = otherProperty?.PropertyType.Alias; + if (otherProperty is not null && alias is not null) + { + foreach (IPropertyValue pvalue in otherProperty.Values) { - foreach (var pvalue in otherProperty.Values) + if (otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, true) && + (culture == "*" || (pvalue.Culture?.InvariantEquals(culture) ?? false))) { - 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 - - if (culture == "*") - { - content.CultureInfos?.Clear(); - content.CultureInfos = null; - } + // copy names, too + if (culture == "*") + { + content.CultureInfos?.Clear(); + content.CultureInfos = null; + } - if (culture == null || culture == "*") - content.Name = other.Name; + if (culture == null || culture == "*") + { + content.Name = other.Name; + } - // ReSharper disable once UseDeconstruction - if (other.CultureInfos is not null) + // ReSharper disable once UseDeconstruction + if (other.CultureInfos is not null) + { + foreach (ContentCultureInfos cultureInfo in other.CultureInfos) { - 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)); + throw new ArgumentNullException(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)); + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); + } - content.PublishCultureInfos?.AddOrUpdate(culture, name, date); + if (culture == null) + { + throw new ArgumentNullException(nameof(culture)); } - // sets the edited cultures on the content - public static void SetCultureEdited(this IContent content, IEnumerable? cultures) + if (string.IsNullOrWhiteSpace(culture)) { - 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(culture)); } - /// - /// 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) + 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) { - if (impact == null) throw new ArgumentNullException(nameof(impact)); + content.EditedCultures = null; + } + else + { + var editedCultures = new HashSet( + cultures.Where(x => !x.IsNullOrWhiteSpace())!, + StringComparer.OrdinalIgnoreCase); + content.EditedCultures = editedCultures.Count > 0 ? editedCultures : null; + } + } - // 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}\"."); + /// + /// 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)); + } - // set names - if (impact.ImpactsAllCultures) + // 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)) { - var name = content.GetCultureName(c); - if (string.IsNullOrWhiteSpace(name)) - return false; - content.SetPublishInfo(c, name, DateTime.Now); + return false; } + + content.SetPublishInfo(c, name, DateTime.Now); } - else if (impact.ImpactsOnlyInvariantCulture) + } + else if (impact.ImpactsOnlyInvariantCulture) + { + if (string.IsNullOrWhiteSpace(content.Name)) { - if (string.IsNullOrWhiteSpace(content.Name)) - return false; - // PublishName set by repository - nothing to do here + return false; } - else if (impact.ImpactsExplicitCulture) + + // PublishName set by repository - nothing to do here + } + else if (impact.ImpactsExplicitCulture) + { + var name = content.GetCultureName(impact.Culture); + if (string.IsNullOrWhiteSpace(name)) { - var name = content.GetCultureName(impact.Culture); - if (string.IsNullOrWhiteSpace(name)) - return false; - content.SetPublishInfo(impact.Culture, name, DateTime.Now); + 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) - { - // for the specified culture (null or all or specific) - property.PublishValues(impact.Culture); + content.SetPublishInfo(impact.Culture, name, DateTime.Now); + } - // 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); - } + // 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); - content.PublishedState = PublishedState.Publishing; - return true; + // 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); + } } - /// - /// Returns false if the culture is already unpublished - /// - /// - /// - /// - public static bool UnpublishCulture(this IContent content, string? culture = "*") + 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)) { - culture = culture?.NullOrWhiteSpaceAsNull(); + throw new NotSupportedException( + $"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); + } - // 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; - var keepProcessing = true; + if (culture == "*") + { + // all cultures + content.ClearPublishInfos(); + } + else + { + // one single culture + keepProcessing = content.ClearPublishInfo(culture); + } - if (culture == "*") - { - // all cultures - content.ClearPublishInfos(); - } - else + if (keepProcessing) + { + // property.PublishValues only publishes what is valid, variation-wise + foreach (IProperty property in content.Properties) { - // one single culture - keepProcessing = content.ClearPublishInfo(culture); + property.UnpublishValues(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; + } - content.PublishedState = PublishedState.Publishing; - } + return keepProcessing; + } - return keepProcessing; - } + public static void ClearPublishInfos(this IContent content) => content.PublishCultureInfos = null; - public static void ClearPublishInfos(this IContent content) + /// + /// Returns false if the culture is already unpublished + /// + /// + /// + /// + public static bool ClearPublishInfo(this IContent content, string? culture) + { + if (culture == null) { - content.PublishCultureInfos = null; + throw new ArgumentNullException(nameof(culture)); } - /// - /// Returns false if the culture is already unpublished - /// - /// - /// - /// - public static bool ClearPublishInfo(this IContent content, string? culture) + if (string.IsNullOrWhiteSpace(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)); + 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; + 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; } } diff --git a/src/Umbraco.Core/Models/ContentSchedule.cs b/src/Umbraco.Core/Models/ContentSchedule.cs index 77526f254a3b..18d254a9aabe 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; - } + 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; - } + /// + /// 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 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 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 date of the scheduled action. - /// - [DataMember] - public DateTime Date { get; } + /// + /// Gets the action to take. + /// + [DataMember] + public ContentScheduleAction Action { 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 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 bool Equals(ContentSchedule other) => - public object DeepClone() - { - return new ContentSchedule(Id, Culture, Date, Action); - } - } + // 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 03be526814ed..d6a50b994b7a 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 12a53fd103ee..4fb90779de5c 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 -{ - 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 Dictionary>(StringComparer.InvariantCultureIgnoreCase); +namespace Umbraco.Cms.Core.Models; - public event NotifyCollectionChangedEventHandler? CollectionChanged; +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); - /// - /// Clears all event handlers - /// - public void ClearCollectionChangedEvents() => CollectionChanged = null; + public event NotifyCollectionChangedEventHandler? CollectionChanged; - private void OnCollectionChanged(NotifyCollectionChangedEventArgs args) - { - CollectionChanged?.Invoke(this, args); - } + /// + /// Returns all schedules registered + /// + /// + public IReadOnlyList FullSchedule => _schedule.SelectMany(x => x.Value.Values).ToList(); - /// - /// Add an existing schedule - /// - /// - public void Add(ContentSchedule schedule) + public object DeepClone() + { + var clone = new ContentScheduleCollection(); + foreach (KeyValuePair> cultureSched in _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 Add(string.Empty, releaseDate, expireDate); - } + return clone; + } - /// - /// Adds a new schedule for a culture - /// - /// - /// - /// - /// true if successfully added, false if validation fails - public bool Add(string? culture, DateTime? releaseDate, DateTime? expireDate) + public bool Equals(ContentScheduleCollection? other) + { + if (other == null) { - if (culture == null) throw new ArgumentNullException(nameof(culture)); - if (releaseDate.HasValue && expireDate.HasValue && releaseDate >= expireDate) - return false; + return false; + } - if (!releaseDate.HasValue && !expireDate.HasValue) return false; + Dictionary> thisSched = _schedule; + Dictionary> thatSched = other._schedule; - // TODO: Do we allow passing in a release or expiry date that is before now? + if (thisSched.Count != thatSched.Count) + { + return false; + } - if (!_schedule.TryGetValue(culture, out var changes)) + 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)) { - changes = new SortedList(); - _schedule[culture] = changes; + return false; } + } - // 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? + return true; + } - if (releaseDate.HasValue) - { - var entry = new ContentSchedule(culture, releaseDate.Value, ContentScheduleAction.Release); - changes.Add(releaseDate.Value, entry); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); - } + public static ContentScheduleCollection CreateWithEntry(DateTime? release, DateTime? expire) + { + var schedule = new ContentScheduleCollection(); + schedule.Add(string.Empty, release, expire); + return schedule; + } - if (expireDate.HasValue) - { - var entry = new ContentSchedule(culture, expireDate.Value, ContentScheduleAction.Expire); - changes.Add(expireDate.Value, entry); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); - } + /// + /// Clears all event handlers + /// + public void ClearCollectionChangedEvents() => CollectionChanged = null; - return true; + /// + /// Add an existing schedule + /// + /// + public void Add(ContentSchedule schedule) + { + if (!_schedule.TryGetValue(schedule.Culture, out SortedList? changes)) + { + changes = new SortedList(); + _schedule[schedule.Culture] = changes; } - /// - /// Remove a scheduled change - /// - /// - public void Remove(ContentSchedule change) - { - if (_schedule.TryGetValue(change.Culture, out var s)) - { - var removed = s.Remove(change.Date); - if (removed) - { - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, change)); - if (s.Count == 0) - _schedule.Remove(change.Culture); - } + // TODO: Below will throw if there are duplicate dates added, validate/return bool? + changes.Add(schedule.Date, schedule); - } - } + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, 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) + 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) { - Clear(string.Empty, action, changeDate); + throw new ArgumentNullException(nameof(culture)); } - /// - /// 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 (releaseDate.HasValue && expireDate.HasValue && releaseDate >= expireDate) { - 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)); - } + return false; + } - if (schedules.Count == 0) - _schedule.Remove(culture); + if (!releaseDate.HasValue && !expireDate.HasValue) + { + return false; } - /// - /// Returns all pending schedules based on the date and type provided - /// - /// - /// - /// - public IReadOnlyList GetPending(ContentScheduleAction action, DateTime date) + // TODO: Do we allow passing in a release or expiry date that is before now? + if (!_schedule.TryGetValue(culture, out SortedList? changes)) { - return _schedule.Values.SelectMany(x => x.Values).Where(x => x.Date <= date).ToList(); + changes = new SortedList(); + _schedule[culture] = changes; } - /// - /// Gets the schedule for invariant content - /// - /// - public IEnumerable GetSchedule(ContentScheduleAction? action = null) + // 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) { - return GetSchedule(string.Empty, action); + var entry = new ContentSchedule(culture, releaseDate.Value, ContentScheduleAction.Release); + changes.Add(releaseDate.Value, entry); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); } - /// - /// Gets the schedule for a culture - /// - /// - /// - /// - public IEnumerable GetSchedule(string? culture, ContentScheduleAction? action = null) + if (expireDate.HasValue) { - 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(); + var entry = new ContentSchedule(culture, expireDate.Value, ContentScheduleAction.Expire); + changes.Add(expireDate.Value, entry); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); } - /// - /// Returns all schedules registered - /// - /// - public IReadOnlyList FullSchedule => _schedule.SelectMany(x => x.Value.Values).ToList(); + return true; + } - public object DeepClone() + /// + /// Remove a scheduled change + /// + /// + public void Remove(ContentSchedule change) + { + if (_schedule.TryGetValue(change.Culture, out SortedList? s)) { - var clone = new ContentScheduleCollection(); - foreach(var cultureSched in _schedule) + var removed = s.Remove(change.Date); + if (removed) { - var list = new SortedList(); - foreach (var schedEntry in cultureSched.Value) - list.Add(schedEntry.Key, (ContentSchedule)schedEntry.Value.DeepClone()); - clone._schedule[cultureSched.Key] = list; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, change)); + if (s.Count == 0) + { + _schedule.Remove(change.Culture); + } } - return clone; } + } - public override bool Equals(object? obj) - => obj is ContentScheduleCollection other && Equals(other); - - public bool Equals(ContentScheduleCollection? other) + /// + /// 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)) { - if (other == null) return false; - - var thisSched = _schedule; - var thatSched = other._schedule; + return; + } - if (thisSched.Count != thatSched.Count) - return false; + var removes = schedules.Where(x => x.Value.Action == action && (!date.HasValue || x.Value.Date <= date.Value)) + .ToList(); - foreach (var (culture, thisList) in thisSched) + foreach (KeyValuePair remove in removes) + { + var removed = schedules.Remove(remove.Value.Date); + if (!removed) { - // if culture is missing, or actions differ, false - if (!thatSched.TryGetValue(culture, out var thatList) || !thatList.SequenceEqual(thisList)) - return false; + continue; } - return true; + OnCollectionChanged( + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, remove.Value)); } - public static ContentScheduleCollection CreateWithEntry(DateTime? release, DateTime? expire) + if (schedules.Count == 0) { - var schedule = new ContentScheduleCollection(); - schedule.Add(string.Empty, release, expire); - return schedule; + _schedule.Remove(culture); } + } - public static ContentScheduleCollection CreateWithEntry(string culture, DateTime? release, DateTime? expire) + /// + /// 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)) { - var schedule = new ContentScheduleCollection(); - schedule.Add(culture, release, expire); - return schedule; + 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 15d5d5986121..1fd1eeaa8aea 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 - - /// - /// The document is not trashed, and not published. - /// - [EnumMember] - Unpublished, + [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 0dacd788445e..1d52300460bd 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 9c21cf5e80e3..f4fe617a8355 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 { - /// - /// Represents the content type that a object is based on - /// - [Serializable] - [DataContract(IsReference = true)] - public class ContentType : ContentTypeCompositionBase, IContentTypeWithHistoryCleanup - { - public const bool SupportsPublishingConst = true; + public const bool SupportsPublishingConst = true; - // Custom comparer for enumerable - private static readonly DelegateEqualityComparer> TemplateComparer = new ( - (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable), - templates => templates.GetHashCode()); + // Custom comparer for enumerable + private static readonly DelegateEqualityComparer> TemplateComparer = new( + (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable), + templates => templates.GetHashCode()); - private IEnumerable? _allowedTemplates; + private IEnumerable? _allowedTemplates; - private int _defaultTemplate; + private int _defaultTemplate; - /// - /// 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) - { - _allowedTemplates = new List(); - HistoryCleanup = new HistoryCleanup(); - } + private HistoryCleanup? _historyCleanup; + /// + /// 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) + { + _allowedTemplates = new List(); + HistoryCleanup = new HistoryCleanup(); + } - /// - /// 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(); - } + /// + /// 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 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); - /// - /// 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); + /// + public override ISimpleContentType ToSimple() => new SimpleContentType(this); + [DataMember] + public int DefaultTemplateId + { + get => _defaultTemplate; + set => SetPropertyValueAndDetectChanges(value, ref _defaultTemplate, nameof(DefaultTemplateId)); + } - [DataMember] - public int 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 { - get => _defaultTemplate; - set => SetPropertyValueAndDetectChanges(value, ref _defaultTemplate, nameof(DefaultTemplateId)); - } + SetPropertyValueAndDetectChanges(value, ref _allowedTemplates, nameof(AllowedTemplates), TemplateComparer); - /// - /// 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 + if (_allowedTemplates?.Any(x => x.Id == _defaultTemplate) == false) { - SetPropertyValueAndDetectChanges(value, ref _allowedTemplates, nameof(AllowedTemplates), TemplateComparer); - - if (_allowedTemplates?.Any(x => x.Id == _defaultTemplate) == false) - { - DefaultTemplateId = 0; - } + DefaultTemplateId = 0; } } + } - private HistoryCleanup? _historyCleanup; + public HistoryCleanup? HistoryCleanup + { + get => _historyCleanup; + set => SetPropertyValueAndDetectChanges(value, ref _historyCleanup, nameof(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 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; - } + /// + /// 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)); - DefaultTemplateId = template.Id; - if (_allowedTemplates?.Any(x => x != null && x.Id == template.Id) == false) - { - var templates = AllowedTemplates?.ToList(); - templates?.Add(template); - AllowedTemplates = templates; - } + /// + /// Sets the default template for the ContentType + /// + /// Default + public void SetDefaultTemplate(ITemplate? template) + { + if (template == null) + { + DefaultTemplateId = 0; + return; } - /// - /// Removes a template from the list of allowed templates - /// - /// to remove - /// True if template was removed, otherwise False - public bool RemoveTemplate(ITemplate template) + DefaultTemplateId = template.Id; + if (_allowedTemplates?.Any(x => x != null && x.Id == template.Id) == false) { - 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); + templates?.Add(template); AllowedTemplates = templates; + } + } - return result; + /// + /// 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; } - /// - IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) => - (IContentType)DeepCloneWithResetIdentities(newAlias); + 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; - /// - public override bool IsDirty() => base.IsDirty() || (HistoryCleanup?.IsDirty() ?? false); + 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 529ae0bbe6f5..c4ab790dfe6c 100644 --- a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs +++ b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs @@ -1,17 +1,17 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Used when determining available compositions for a given content type +/// +public class ContentTypeAvailableCompositionsResult { - /// - /// Used when determining available compositions for a given content type - /// - public class ContentTypeAvailableCompositionsResult + public ContentTypeAvailableCompositionsResult(IContentTypeComposition composition, bool allowed) { - public ContentTypeAvailableCompositionsResult(IContentTypeComposition composition, bool allowed) - { - Composition = composition; - Allowed = allowed; - } - - public IContentTypeComposition Composition { get; private set; } - public bool Allowed { get; private set; } + 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 180552cd7497..4dc268faf368 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; - } + Ancestors = Enumerable.Empty(); + Results = Enumerable.Empty(); + } - public IEnumerable Ancestors { get; private set; } - public IEnumerable Results { get; private set; } + 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 bf7cd0d8e3b7..6131e1b6809d 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; + throw new ArgumentOutOfRangeException(nameof(parentId)); + } - _allowedContentTypes = new List(); - _propertyGroups = new PropertyGroupCollection(); + ParentId = parentId; - // actually OK as IsPublishing is constant - // ReSharper disable once VirtualMemberCallInConstructor - _noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing); - _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; + _allowedContentTypes = new List(); + _propertyGroups = new PropertyGroupCollection(); - _variations = ContentVariation.Nothing; - } + // 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) + : this(shortStringHelper, parent, string.Empty) + { + } - protected ContentTypeBase(IShortStringHelper shortStringHelper, IContentTypeBase parent, string alias) + protected ContentTypeBase(IShortStringHelper shortStringHelper, IContentTypeBase parent, string alias) + { + if (parent == null) { - if (parent == null) throw new ArgumentNullException(nameof(parent)); - SetParent(parent); + throw new ArgumentNullException(nameof(parent)); + } - _shortStringHelper = shortStringHelper; - _alias = alias; - _allowedContentTypes = new List(); - _propertyGroups = new PropertyGroupCollection(); + SetParent(parent); - // actually OK as IsPublishing is constant - // ReSharper disable once VirtualMemberCallInConstructor - _noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing); - _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; + _shortStringHelper = shortStringHelper; + _alias = alias; + _allowedContentTypes = new List(); + _propertyGroups = new PropertyGroupCollection(); - _variations = ContentVariation.Nothing; - } + // actually OK as IsPublishing is constant + // ReSharper disable once VirtualMemberCallInConstructor + PropertyTypeCollection = new PropertyTypeCollection(SupportsPublishing); + PropertyTypeCollection.CollectionChanged += PropertyTypesChanged; - public abstract ISimpleContentType ToSimple(); - - /// - /// 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; } - - //Custom comparer for enumerable - private static readonly DelegateEqualityComparer> ContentTypeSortComparer = - new DelegateEqualityComparer>( - (sorts, enumerable) => sorts.UnsortedSequenceEqual(enumerable), - sorts => sorts.GetHashCode()); - - protected void PropertyGroupsChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(PropertyGroups)); - } + _variations = ContentVariation.Nothing; + } - 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)); - } + /// + /// 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)); - } + /// + /// 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)); + } - /// - /// Description for the ContentType - /// - [DataMember] - public string? Description + /// + /// 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 { - get => _description; - set => SetPropertyValueAndDetectChanges(value, ref _description, nameof(Description)); + _hasPropertyTypeBeenRemoved = value; + OnPropertyChanged(nameof(HasPropertyTypeBeenRemoved)); } + } - /// - /// Name of the icon (sprite class) used to identify the ContentType - /// - [DataMember] - public string? Icon - { - get => _icon; - set => SetPropertyValueAndDetectChanges(value, ref _icon, nameof(Icon)); - } + /// + /// PropertyTypes that are not part of a PropertyGroup + /// + [IgnoreDataMember] - /// - /// Name of the thumbnail used to identify the ContentType - /// - [DataMember] - public string? Thumbnail - { - get => _thumbnail; - set => SetPropertyValueAndDetectChanges(value, ref _thumbnail, nameof(Thumbnail)); - } + // TODO: should we mark this as EditorBrowsable hidden since it really isn't ever used? + internal PropertyTypeCollection PropertyTypeCollection { get; private set; } - /// - /// 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)); - } + public abstract ISimpleContentType ToSimple(); - /// - /// 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)); - } + /// + /// Description for the ContentType + /// + [DataMember] + public string? Description + { + get => _description; + set => SetPropertyValueAndDetectChanges(value, ref _description, nameof(Description)); + } - /// - [DataMember] - public bool IsElement - { - get => _isElement; - set => SetPropertyValueAndDetectChanges(value, ref _isElement, nameof(IsElement)); - } + /// + /// Name of the icon (sprite class) used to identify the ContentType + /// + [DataMember] + public string? Icon + { + get => _icon; + set => SetPropertyValueAndDetectChanges(value, ref _icon, nameof(Icon)); + } - /// - /// 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); - } + /// + /// 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 the content variation of the content type. - /// - public virtual ContentVariation Variations - { - get => _variations; - set => SetPropertyValueAndDetectChanges(value, ref _variations, nameof(Variations)); - } + /// + /// 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)); + } - /// - 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); - } + /// + /// 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)); + } - /// - 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); - } + /// + [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 + /// + /// + /// 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 { - get => _propertyGroups; - set - { - _propertyGroups = value; - _propertyGroups.CollectionChanged += PropertyGroupsChanged; - PropertyGroupsChanged(_propertyGroups, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } + _propertyGroups = value; + _propertyGroups.CollectionChanged += PropertyGroupsChanged; + PropertyGroupsChanged( + _propertyGroups, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } + } + + /// + public bool SupportsVariation(string culture, string segment, bool wildcards = false) => - /// - [IgnoreDataMember] - [DoNotClone] - public IEnumerable PropertyTypes + // 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 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, true, false); + + /// + [IgnoreDataMember] + [DoNotClone] + public IEnumerable PropertyTypes => + PropertyTypeCollection.Union(PropertyGroups.SelectMany(x => x.PropertyTypes!)); + + /// + [DoNotClone] + public IEnumerable NoGroupPropertyTypes + { + get => PropertyTypeCollection; + set { - get + if (PropertyTypeCollection != null) { - return _noGroupPropertyTypes.Union(PropertyGroups.SelectMany(x => x.PropertyTypes!)); + PropertyTypeCollection.ClearCollectionChangedEvents(); } + + PropertyTypeCollection = new PropertyTypeCollection(SupportsPublishing, value); + PropertyTypeCollection.CollectionChanged += PropertyTypesChanged; + PropertyTypesChanged( + PropertyTypeCollection, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } + } - /// - [DoNotClone] - public IEnumerable NoGroupPropertyTypes - { - get => _noGroupPropertyTypes; - set - { - if (_noGroupPropertyTypes != null) - { - _noGroupPropertyTypes.ClearCollectionChangedEvents(); - } + /// + /// 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); - _noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing, value); - _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; - PropertyTypesChanged(_noGroupPropertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - } + /// + public abstract bool AddPropertyGroup(string alias, string name); + + /// + public abstract bool AddPropertyType(IPropertyType propertyType, string propertyGroupAlias, string? propertyGroupName = null); - /// - /// 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 + /// + /// 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 => _hasPropertyTypeBeenRemoved; - private set - { - _hasPropertyTypeBeenRemoved = value; - OnPropertyChanged(nameof(HasPropertyTypeBeenRemoved)); - } + PropertyTypeCollection.Add(propertyType); + return true; } - /// - /// 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; - } + 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) + { 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 new group, if required, and ensure it exists + PropertyGroup? newPropertyGroup = null; + if (propertyGroupAlias != null) { - // 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) { - var index = PropertyGroups.IndexOfKey(propertyGroupAlias); - if (index == -1) return false; - - newPropertyGroup = PropertyGroups[index]; + return false; } - // get old group - var oldPropertyGroup = PropertyGroups.FirstOrDefault(x => - x.PropertyTypes?.Any(y => y.Alias == propertyTypeAlias) ?? false); + newPropertyGroup = PropertyGroups[index]; + } - // set new group - propertyType.PropertyGroupId = newPropertyGroup == null ? null : new Lazy(() => newPropertyGroup.Id, false); + // get old group + PropertyGroup? oldPropertyGroup = PropertyGroups.FirstOrDefault(x => x.PropertyTypes?.Any(y => y.Alias == propertyTypeAlias) ?? false); - // remove from old group, if any - add to new group, if any - oldPropertyGroup?.PropertyTypes?.RemoveItem(propertyTypeAlias); - newPropertyGroup?.PropertyTypes?.Add(propertyType); + // set new group + propertyType.PropertyGroupId = + newPropertyGroup == null ? null : new Lazy(() => newPropertyGroup.Id, false); - return true; - } + // remove from old group, if any - add to new group, if any + oldPropertyGroup?.PropertyTypes?.RemoveItem(propertyTypeAlias); + newPropertyGroup?.PropertyTypes?.Add(propertyType); - /// - /// 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 (var propertyGroup in PropertyGroups) - { - if (propertyGroup.PropertyTypes?.RemoveItem(alias) ?? false) - { - if (!HasPropertyTypeBeenRemoved) - { - HasPropertyTypeBeenRemoved = true; - OnPropertyChanged(nameof(PropertyTypes)); - } - break; - } - } + return true; + } - //check through each local property type collection (not assigned to a tab) - if (_noGroupPropertyTypes.RemoveItem(alias)) + /// + /// 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; + } + } + + // check through each local property type collection (not assigned to a tab) + if (PropertyTypeCollection.RemoveItem(alias)) + { + if (!HasPropertyTypeBeenRemoved) + { + HasPropertyTypeBeenRemoved = true; + OnPropertyChanged(nameof(PropertyTypes)); } } + } - /// - /// Removes a PropertyGroup from the current ContentType - /// - /// Alias of the to remove - public void RemovePropertyGroup(string alias) + /// + /// 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) { - // if no group exists with that alias, do nothing - var index = PropertyGroups.IndexOfKey(alias); - if (index == -1) return; + return; + } - var group = PropertyGroups[index]; + PropertyGroup group = PropertyGroups[index]; - // first remove the group - PropertyGroups.Remove(group); + // first remove the group + PropertyGroups.Remove(group); - if (group.PropertyTypes is not null) + if (group.PropertyTypes is not null) + { + // Then re-assign the group's properties to no group + foreach (IPropertyType property in group.PropertyTypes) { - // Then re-assign the group's properties to no group - foreach (var property in group.PropertyTypes) - { - property.PropertyGroupId = null; - _noGroupPropertyTypes.Add(property); - } + property.PropertyGroupId = null; + PropertyTypeCollection.Add(property); } - - 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() - { - bool dirtyEntity = base.IsDirty(); + OnPropertyChanged(nameof(PropertyGroups)); + } - bool dirtyGroups = PropertyGroups.Any(x => x.IsDirty()); - bool dirtyTypes = PropertyTypes.Any(x => x.IsDirty()); + /// + /// Indicates whether the current entity is dirty. + /// + /// True if entity is dirty, otherwise False + public override bool IsDirty() + { + var dirtyEntity = base.IsDirty(); - return dirtyEntity || dirtyGroups || dirtyTypes; - } + var dirtyGroups = PropertyGroups.Any(x => x.IsDirty()); + var dirtyTypes = PropertyTypes.Any(x => x.IsDirty()); - /// - /// 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(); + return dirtyEntity || dirtyGroups || dirtyTypes; + } - //loop through each property group to reset the property types - var propertiesReset = new List(); + /// + /// 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(); - foreach (var propertyGroup in PropertyGroups) + // 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) { - propertyGroup.ResetDirtyProperties(); - if (propertyGroup.PropertyTypes is not null) + foreach (IPropertyType propertyType in propertyGroup.PropertyTypes) { - foreach (var propertyType in propertyGroup.PropertyTypes) - { - propertyType.ResetDirtyProperties(); - propertiesReset.Add(propertyType.Id); - } + 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(); - } + // 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)) + { + propertyType.ResetDirtyProperties(); } + } - protected override void PerformDeepClone(object clone) + public ContentTypeBase DeepCloneWithResetIdentities(string alias) + { + var clone = (ContentTypeBase)DeepClone(); + clone.Alias = alias; + clone.Key = Guid.Empty; + foreach (PropertyGroup propertyGroup in clone.PropertyGroups) { - base.PerformDeepClone(clone); + propertyGroup.ResetIdentity(); + propertyGroup.ResetDirtyProperties(false); + } - var clonedEntity = (ContentTypeBase) clone; + foreach (IPropertyType propertyType in clone.PropertyTypes) + { + propertyType.ResetIdentity(); + propertyType.ResetDirtyProperties(false); + } - 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 + clone.ResetIdentity(); + clone.ResetDirtyProperties(false); + return clone; + } - 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 - } + 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); - 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 - } - } + var clonedEntity = (ContentTypeBase)clone; - public ContentTypeBase DeepCloneWithResetIdentities(string alias) + if (clonedEntity.PropertyTypeCollection != 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); - } + // 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 + } - clone.ResetIdentity(); - clone.ResetDirtyProperties(false); - return clone; + 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 } } } diff --git a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs index d771efa12b0f..12e0e5a13823 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; } - - /// - /// Used to check if any property type was changed between variant/invariant - /// - /// - /// - public static bool WasPropertyTypeVariationChanged(this IContentTypeBase contentType) + else if (typeof(IContentType).IsAssignableFrom(type)) { - return contentType.WasPropertyTypeVariationChanged(out var _); + itemType = PublishedItemType.Content; } - - /// - /// Used to check if any property type was changed between variant/invariant - /// - /// - /// - internal static bool WasPropertyTypeVariationChanged(this IContentTypeBase contentType, out IReadOnlyCollection aliases) + else if (typeof(IMediaType).IsAssignableFrom(type)) { - var a = new List(); + itemType = PublishedItemType.Media; + } + else if (typeof(IMemberType).IsAssignableFrom(type)) + { + itemType = PublishedItemType.Member; + } - // 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) return false; + return itemType; + } - // variation change? - var dirty = propertyType.WasPropertyDirty("Variations"); - if (dirty) - a.Add(propertyType.Alias); + /// + /// Used to check if any property type was changed between variant/invariant + /// + /// + /// + public static bool WasPropertyTypeVariationChanged(this IContentTypeBase contentType) => + contentType.WasPropertyTypeVariationChanged(out IReadOnlyCollection _); + + /// + /// 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(); - return dirty; + // 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) + { + return false; + } - }); + // variation change? + var dirty = propertyType.WasPropertyDirty("Variations"); + if (dirty) + { + a.Add(propertyType.Alias); + } - aliases = a; - return hasAnyPropertyVariationChanged; - } + return dirty; + }); + + aliases = a; + return hasAnyPropertyVariationChanged; } } diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index 18dc1189f2e8..b7b9af6231c4 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, IContentTypeComposition parent) + : this(shortStringHelper, parent, string.Empty) + { + } - protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, int parentId) - : base(shortStringHelper, parentId) - { } + protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, IContentTypeComposition parent, string alias) + : base(shortStringHelper, parent, alias) => + AddContentType(parent); - protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper,IContentTypeComposition parent) - : this(shortStringHelper, parent, string.Empty) - { } + public IEnumerable RemovedContentTypes => _removedContentTypeKeyTracker; - protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, IContentTypeComposition parent, string alias) - : base(shortStringHelper, parent, alias) + /// + /// 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); + return false; + } - if (!start) - propertyTypes = propertyTypes.Union(PropertyTypes); + if (contentType.ContentTypeComposition.Any(x => x.CompositionAliases().Any(ContentTypeCompositionExists))) + { + return false; + } - return propertyTypes; + if (string.IsNullOrEmpty(Alias) == false && Alias.Equals(contentType.Alias)) + { + return false; } - /// - /// 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 (ContentTypeCompositionExists(contentType.Alias) == false) { - if (contentType is 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(); + + if (conflictingPropertyTypeAliases.Any()) { - return false; + throw new InvalidCompositionException(Alias, contentType.Alias, conflictingPropertyTypeAliases.ToArray()); } - if (contentType.ContentTypeComposition.Any(x => x.CompositionAliases().Any(ContentTypeCompositionExists))) - return false; - if (string.IsNullOrEmpty(Alias) == false && Alias.Equals(contentType.Alias)) - return false; + _contentTypeComposition.Add(contentType); - 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(); + OnPropertyChanged(nameof(ContentTypeComposition)); - if (conflictingPropertyTypeAliases.Any()) - throw new InvalidCompositionException(Alias, contentType.Alias, conflictingPropertyTypeAliases.ToArray()); + return true; + } - _contentTypeComposition.Add(contentType); + return false; + } - OnPropertyChanged(nameof(ContentTypeComposition)); + /// + /// 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)) + { + IContentTypeComposition? contentTypeComposition = ContentTypeComposition.FirstOrDefault(x => x.Alias == alias); - return true; + // You can't remove a composition from another composition + if (contentTypeComposition == null) + { + return false; } - 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); } - /// - /// 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) + 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))) { - 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; + return true; + } - _removedContentTypeKeyTracker.Add(contentTypeComposition.Id); + if (ContentTypeComposition.Any(x => x.ContentTypeCompositionExists(alias))) + { + return true; + } - // 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); + return false; + } - OnPropertyChanged(nameof(ContentTypeComposition)); + /// + /// 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); - return _contentTypeComposition.Remove(contentTypeComposition); - } + /// + 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; } - /// - /// 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) + // get and ensure a group local to this content type + PropertyGroup? group; + var index = PropertyGroups.IndexOfKey(propertyGroupAlias); + if (index != -1) { - if (ContentTypeComposition.Any(x => x.Alias.Equals(alias))) - return true; - - if (ContentTypeComposition.Any(x => x.ContentTypeCompositionExists(alias))) - return true; - + 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; } - /// - /// 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); + // add property to group + propertyType.PropertyGroupId = new Lazy(() => group.Id); + group.PropertyTypes?.Add(propertyType); - /// - public override bool AddPropertyGroup(string alias, string name) => AddAndReturnPropertyGroup(alias, name) != null; + return true; + } - private PropertyGroup? AddAndReturnPropertyGroup(string alias, string name) - { - // Ensure we don't have it already - if (PropertyGroups.Contains(alias)) - return null; + /// + /// 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())); - // 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 - 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; - } + /// + /// 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; - // add - PropertyGroups.Add(group); + // 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(); + } - return group; + private IEnumerable GetRawComposedPropertyTypes(bool start = true) + { + IEnumerable propertyTypes = ContentTypeComposition + .Cast() + .SelectMany(x => start ? x.GetRawComposedPropertyTypes(false) : x.CompositionPropertyTypes); + + if (!start) + { + propertyTypes = propertyTypes.Union(PropertyTypes); } - /// - public override bool AddPropertyType(IPropertyType propertyType, string propertyGroupAlias, string? propertyGroupName = null) + return propertyTypes; + } + + private PropertyGroup? AddAndReturnPropertyGroup(string alias, string name) + { + // Ensure we don't have it already + if (PropertyGroups.Contains(alias)) { - // ensure no duplicate alias - over all composition properties - if (PropertyTypeExists(propertyType.Alias)) - return false; + return null; + } - // 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 + // 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) { - // No group name specified, so we can't create a new one and add the property type - return false; + group.SortOrder = lastGroup.SortOrder + 1; } - - // 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) + else { - base.PerformDeepClone(clone); + // yes, inherited, re-use sort order + group.SortOrder = inheritGroup.SortOrder; + } - var clonedEntity = (ContentTypeCompositionBase)clone; + // add + PropertyGroups.Add(group); - // 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(); - } + return group; } } diff --git a/src/Umbraco.Core/Models/ContentTypeImportModel.cs b/src/Umbraco.Core/Models/ContentTypeImportModel.cs index 49d09c68213c..5de62fcffa68 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 e7a11bad47c1..e10d650cac8d 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 { + // this parameterless ctor should never be used BUT is required by AutoMapper in EntityMapperProfile + public ContentTypeSort() + { + } + /// - /// Represents a POCO for setting sort order on a ContentType reference + /// Initializes a new instance of the class. /// - public class ContentTypeSort : IValueObject, IDeepCloneable + public ContentTypeSort(int id, int sortOrder) { - // 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) - { - Id = new Lazy(() => id); - SortOrder = sortOrder; - } + Id = new Lazy(() => id); + SortOrder = sortOrder; + } - public ContentTypeSort(Lazy id, int sortOrder, string @alias) - { - Id = id; - SortOrder = sortOrder; - Alias = alias; - } + 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 Lazy(() => 0); + /// + /// 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 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; + /// + /// 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 object DeepClone() + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - var clone = (ContentTypeSort)MemberwiseClone(); - var id = Id.Value; - clone.Id = new Lazy(() => id); - return clone; + return false; } - protected bool Equals(ContentTypeSort other) + if (ReferenceEquals(this, obj)) { - return Id.Value.Equals(other.Id.Value) && string.Equals(Alias, other.Alias); + return true; } - public override bool Equals(object? obj) + if (obj.GetType() != GetType()) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((ContentTypeSort) obj); + return false; } - public override int GetHashCode() + return Equals((ContentTypeSort)obj); + } + + protected bool Equals(ContentTypeSort other) => + Id.Value.Equals(other.Id.Value) && string.Equals(Alias, other.Alias); + + 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 00c7f197a807..019da0eee01e 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 5fa0e9895822..7d7cc6c57832 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 dbcd8540a0ed..cf95257716ce 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 fec02093d7f3..a6e83a7b7c18 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. + /// + /// + /// 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. /// /// - /// A set of cultures can be either all cultures (including the invariant culture), or - /// the invariant culture, or a specific culture. + /// And then it does not impact the invariant culture. The impacted + /// explicit culture could be the default culture. /// - public sealed class CultureImpact + 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; + // null can only be invariant + if (Culture == null) + { + return Behavior.InvariantCulture | Behavior.InvariantProperties; + } + + // * 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 + Behavior result = Behavior.ExplicitCulture; + + // if the explicit culture is the default, then the behavior is also InvariantProperties + if (ImpactsOnlyDefaultCulture) + { + result |= Behavior.InvariantProperties; + } + + return result; } + } - /// - /// 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) + /// + /// 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) { - if (culture != null && culture.IsNullOrWhiteSpace()) - throw new ArgumentException("Culture \"\" is not valid here."); - - Culture = culture; + throw new ArgumentNullException(nameof(content)); + } - if ((culture == null || culture == "*") && isDefault) - throw new ArgumentException("The invariant or 'all' culture can not be the default culture."); + if (savingCultures == null) + { + throw new ArgumentNullException(nameof(savingCultures)); + } - ImpactsOnlyDefaultCulture = isDefault; + if (savingCultures.Length == 0) + { + throw new ArgumentException(nameof(savingCultures)); } - /// - /// 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) + 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) { - 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); + throw new ArgumentException("Culture is not explicit."); } - /// - /// 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) + if (culture.IsNullOrWhiteSpace()) { - // throws if not successful - TryCreate(culture, isDefault, content.ContentType.Variations, true, out var impact); - return impact; + throw new ArgumentException("Culture \"\" is not explicit."); } - /// - /// 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) + if (culture == "*") { - impact = null; + throw new ArgumentException("Culture \"*\" is not explicit."); + } - // if culture is 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; - } + return new CultureImpact(culture, isDefault); + } - // ... and it cannot be default - if (isDefault) - { - if (throwOnFail) - throw new InvalidOperationException("The invariant culture can not be the default culture."); - return false; - } + /// + /// 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; + } - impact = Invariant; - return true; - } + /// + /// 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 'all'... - if (culture == "*") + // if culture is invariant... + if (culture == null) + { + // ... then variation must not vary by culture ... + if (variation.VariesByCulture()) { - // ... it cannot be default - if (isDefault) + if (throwOnFail) { - if (throwOnFail) - throw new InvalidOperationException("The 'all' culture can not be the default culture."); - return false; + throw new InvalidOperationException( + "The invariant culture is not compatible with a varying variation."); } - // if variation does not vary by culture, then impact is invariant - impact = variation.VariesByCulture() ? All : Invariant; - return true; + return false; } - // neither null nor "*" - cannot be the empty string - if (culture.IsNullOrWhiteSpace()) + // ... and it cannot be default + if (isDefault) { if (throwOnFail) - throw new ArgumentException("Cannot be the empty string.", nameof(culture)); + { + throw new InvalidOperationException("The invariant culture can not be the default culture."); + } + return false; } - // if culture is specific, then variation must vary - if (!variation.VariesByCulture()) + impact = Invariant; + return true; + } + + // if culture is 'all'... + if (culture == "*") + { + // ... 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 'all' culture can not be the default culture."); + } + return false; } - // return specific impact - impact = new CultureImpact(culture, isDefault); + // if variation does not vary by culture, then impact is invariant + impact = variation.VariesByCulture() ? All : 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 + // neither null nor "*" - cannot be the empty string + if (culture.IsNullOrWhiteSpace()) { - get + if (throwOnFail) { - //null can only be invariant - if (Culture == null) return Behavior.InvariantCulture | Behavior.InvariantProperties; - - // * 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; + throw new ArgumentException("Cannot be the empty string.", nameof(culture)); } - } + return false; + } - [Flags] - public enum Behavior : byte + // if culture is specific, then variation must vary + if (!variation.VariesByCulture()) { - AllCultures = 1, - InvariantCulture = 2, - ExplicitCulture = 4, - InvariantProperties = 8 + 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 6b33f0738553..630ef338bd63 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; - - /// - /// Initializes a new instance of the class. - /// - public DataType(IDataEditor? editor, IConfigurationEditorJsonSerializer serializer, int parentId = -1) - { - _editor = editor ?? throw new ArgumentNullException(nameof(editor)); - _serializer = serializer ?? throw new ArgumentNullException(nameof(editor)); - ParentId = parentId; + _editor = editor ?? throw new ArgumentNullException(nameof(editor)); + _serializer = serializer ?? throw new ArgumentNullException(nameof(editor)); + ParentId = parentId; - // set a default configuration - Configuration = _editor.GetConfigurationEditor().DefaultConfigurationObject; - } + // set a default configuration + Configuration = _editor.GetConfigurationEditor().DefaultConfigurationObject; + } - /// - [IgnoreDataMember] - public IDataEditor? Editor + /// + [IgnoreDataMember] + public IDataEditor? Editor + { + get => _editor; + set { - 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)); + return; + } - // 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; + OnPropertyChanged(nameof(Editor)); - 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); - } + // 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); } } + } - /// - [DataMember] - public string EditorAlias => _editor?.Alias ?? string.Empty; + /// + [DataMember] + public string EditorAlias => _editor?.Alias ?? string.Empty; - /// - [DataMember] - public ValueStorageType DatabaseType + /// + [DataMember] + public ValueStorageType DatabaseType + { + get => _databaseType; + set => SetPropertyValueAndDetectChanges(value, ref _databaseType, nameof(DatabaseType)); + } + + /// + [DataMember] + public object? Configuration + { + get { - get => _databaseType; - set => SetPropertyValueAndDetectChanges(value, ref _databaseType, nameof(DatabaseType)); + // 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; } - /// - [DataMember] - public object? Configuration + set { - get + if (value == null) { - // 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; + throw new ArgumentNullException(nameof(value)); + } - 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); - } + // 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)); + } - _hasConfiguration = true; - _configurationJson = null; + // 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)); + } - return _configuration; + // extract database type from configuration object, if appropriate + if (value is IConfigureValueType valueTypeConfiguration) + { + DatabaseType = ValueTypes.ToStorageType(valueTypeConfiguration.ValueType); } - set + + // 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)) { - 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)); + 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) + /// + /// 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) { - _hasConfiguration = false; - _configuration = null; - _configurationJson = configurationJson; + // if configuration has already been de-serialized, return + var capturedConfiguration = _configuration; + return new Lazy(() => capturedConfiguration); } - - /// - /// 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() + else { - // 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) + // else, create a Lazy de-serializer + var capturedConfiguration = _configurationJson; + IDataEditor? capturedEditor = _editor; + return new Lazy(() => { - // 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 { - 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); - } - }); - } + 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 10419dca881e..791f7b248b42 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 { + 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, + }; + /// - /// Provides extensions methods for . + /// Gets the configuration object. /// - public static class DataTypeExtensions + /// 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 { - /// - /// 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, - }; - - /// - /// Returns true if this date type is build-in/default. - /// - /// The data type definition. - /// - public static bool IsBuildInDataType(this IDataType dataType) - { - return IsBuildInDataType(dataType.Key); - } + var configuration = dataType.Configuration; - /// - /// Returns true if this date type is build-in/default. - /// - public static bool IsBuildInDataType(Guid key) + switch (configuration) { - 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 4dc293641ce0..ce34dab6f12b 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 +namespace Umbraco.Cms.Core.Models; + +public static class DeepCloneHelper { - 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) { - /// - /// 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; - } + Type inputType = input.GetType(); + Type outputType = output.GetType(); - public PropertyInfo PropertyInfo { get; private set; } - public bool IsDeepCloneable { get; set; } - public Type? GenericListType { get; set; } - public bool IsList - { - get { return GenericListType != null; } - } + 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(); - - /// - /// 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(); + // 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 ( - if (inputType != outputType) - { - throw new InvalidOperationException("Both the input and output types must be the same"); - } + // 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; + } - //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 => + if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType)) { - 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 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<>))) { - return null; + // 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) }; + } - if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType)) + // skip instead of trying to create instance of abstract or interface + if (propertyInfo.PropertyType.IsAbstract || propertyInfo.PropertyType.IsInterface) { - return new ClonePropertyInfo(propertyInfo) { IsDeepCloneable = true }; + return null; } - if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) - && TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) == false) + // its a custom IEnumerable, we'll try to create it + try { - 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) - { - return null; - } + var custom = Activator.CreateInstance(propertyInfo.PropertyType); - //its a custom IEnumerable, we'll try to create it - try + // if it's an IList we can work with it, otherwise we cannot + if (custom is not IList) { - 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; + // 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; + } + } - if (clonePropertyInfo.PropertyInfo.PropertyType.IsArray) + // 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++) { - //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) { - if (arr != null) - { - arr[i] = newList[i]; - } + 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); } + // 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 14cd3bb2e58f..7473cef60f13 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 + // Custom comparer for enumerable + private static readonly DelegateEqualityComparer> + DictionaryTranslationComparer = + new( + (enumerable, translations) => enumerable.UnsortedSequenceEqual(translations), + enumerable => enumerable.GetHashCode()); + + private string _itemKey; + private Guid? _parentId; + private IEnumerable _translations; + + public DictionaryItem(string itemKey) + : this(null, itemKey) { - 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(); + } - public DictionaryItem(Guid? parentId, string itemKey) - { - _parentId = parentId; - _itemKey = itemKey; - _translations = new List(); - } + public Func? GetLanguage { get; set; } - //Custom comparer for enumerable - private static readonly DelegateEqualityComparer> DictionaryTranslationComparer = - new DelegateEqualityComparer>( - (enumerable, translations) => enumerable.UnsortedSequenceEqual(translations), - enumerable => enumerable.GetHashCode()); + /// + /// 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 Parent Id of the Dictionary Item - /// - [DataMember] - public Guid? ParentId - { - get { return _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 the Key for the Dictionary Item - /// - [DataMember] - public string ItemKey + /// + /// Gets or sets a list of translations for the Dictionary Item + /// + [DataMember] + public IEnumerable Translations + { + get => _translations; + set { - get { return _itemKey; } - set { SetPropertyValueAndDetectChanges(value, ref _itemKey!, nameof(ItemKey)); } - } + IDictionaryTranslation[] asArray = value.ToArray(); - /// - /// Gets or sets a list of translations for the Dictionary Item - /// - [DataMember] - public IEnumerable Translations - { - get { return _translations; } - set + // 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 137680aa27f1..3e6c05120174 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 +namespace Umbraco.Extensions; + +public static class DictionaryItemExtensions { - 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) { - /// - /// 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; - } + 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) - { - var defaultTranslation = d.Translations?.FirstOrDefault(x => x.Language?.Id == 1); - return defaultTranslation == null ? string.Empty : defaultTranslation.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 d0d98a64dbc9..5d44768388c3 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 - { - public Func? GetLanguage { get; set; } + private ILanguage? _language; - private ILanguage? _language; - private string _value; - //note: this will be memberwise cloned - private int _languageId; + // note: this will be memberwise cloned + private string _value; - public DictionaryTranslation(ILanguage language, string value) - { - if (language == null) throw new ArgumentNullException("language"); - _language = language; - _languageId = _language.Id; - _value = value; - } + public DictionaryTranslation(ILanguage language, string value) + { + _language = language ?? throw new ArgumentNullException("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(ILanguage language, string value, Guid uniqueId) + { + _language = language ?? throw new ArgumentNullException("language"); + LanguageId = _language.Id; + _value = value; + Key = uniqueId; + } - public DictionaryTranslation(int languageId, string value) - { - _languageId = languageId; - _value = 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 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 + /// + /// 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 { - 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); } - } - public int LanguageId - { - get { return _languageId; } + return _language; } - /// - /// Gets or sets the translated text - /// - [DataMember] - public string Value + set { - get { return _value; } - set { SetPropertyValueAndDetectChanges(value, ref _value!, nameof(Value)); } + SetPropertyValueAndDetectChanges(value, ref _language, nameof(Language)); + LanguageId = _language == null ? -1 : _language.Id; } + } - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); + public int LanguageId { get; private set; } - var clonedEntity = (DictionaryTranslation)clone; + /// + /// Gets or sets the translated text + /// + [DataMember] + public string Value + { + get => _value; + set => SetPropertyValueAndDetectChanges(value, ref _value!, nameof(Value)); + } - // clear fields that were memberwise-cloned and that we don't want to clone - clonedEntity._language = null; - } + 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 39a7bcd90025..1fb0b3cd4b46 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 0255cfd40e0d..ac19eef0c8c7 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; - } + Value = value; + DataTypeConfiguration = dataTypeConfiguration; + } - /// - /// The value submitted for the property - /// - public object? Value { get; } + /// + /// The value submitted for the property + /// + public object? Value { get; } - /// - /// The data type configuration for the property. - /// - public object? DataTypeConfiguration { 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 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 unique identifier of the property type. + /// + public Guid PropertyTypeKey { get; set; } - /// - /// Gets or sets the uploaded files. - /// - public ContentPropertyFile[]? Files { 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 d1bc9127ce8c..9bb098697c90 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 +{ /// - /// Represents an uploaded file for a property. + /// Gets or sets the property alias. /// - public class ContentPropertyFile - { - /// - /// Gets or sets the property alias. - /// - public string? PropertyAlias { get; set; } + public string? PropertyAlias { 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 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 4efc5017e113..c093962408c2 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) - { - 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)); - public UmbracoEntityReference(Udi udi) + switch (udi.EntityType) { - 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; - } + case Constants.UdiEntityType.Media: + RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedMediaAlias; + break; + default: + RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias; + break; } + } - public static UmbracoEntityReference Empty() => _empty; + public Udi Udi { get; } - public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty(); + public static UmbracoEntityReference Empty() => _empty; - public Udi Udi { get; } - public string RelationTypeAlias { get; } + public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty(); - public override bool Equals(object? obj) - { - return obj is UmbracoEntityReference reference && Equals(reference); - } + public string RelationTypeAlias { get; } - public bool Equals(UmbracoEntityReference other) - { - return EqualityComparer.Default.Equals(Udi, other.Udi) && - RelationTypeAlias == other.RelationTypeAlias; - } + public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right); - public override int GetHashCode() - { - var hashCode = -487348478; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Udi); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(RelationTypeAlias); - return hashCode; - } + public override bool Equals(object? obj) => obj is UmbracoEntityReference reference && Equals(reference); - public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) - { - return left.Equals(right); - } + public bool Equals(UmbracoEntityReference other) => + EqualityComparer.Default.Equals(Udi, other.Udi) && + RelationTypeAlias == other.RelationTypeAlias; - public static bool operator !=(UmbracoEntityReference left, UmbracoEntityReference right) - { - return !(left == right); - } + 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 b012bbfeb38b..141928541767 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 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)); + + From = from; + To = to; + Cc = cc; + Bcc = bcc; + ReplyTo = replyTo; + Subject = subject; + Body = body; + IsBodyHtml = isBodyHtml; + Attachments = attachments?.ToList(); + } + + public string? From { get; } - public string?[] To { get; } + public string?[] To { get; } - public string[]? Cc { get; } + public string[]? Cc { get; } - public string[]? Bcc { get; } + public string[]? Bcc { get; } - public string[]? ReplyTo { get; } + public string[]? ReplyTo { get; } - public string? Subject { get; } + public string? Subject { get; } - public string? Body { get; } + public string? Body { get; } - public bool IsBodyHtml { get; } + public bool IsBodyHtml { get; } - public IList? Attachments { get; } + public IList? Attachments { get; } - public bool HasAttachments => Attachments != null && Attachments.Count > 0; + public bool HasAttachments => Attachments != null && Attachments.Count > 0; - public EmailMessage(string? from, string? to, string? subject, string? body, bool isBodyHtml) - : this(from, new[] { to }, null, null, null, subject, body, isBodyHtml, null) + 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) + { + throw new ArgumentException("Value cannot be empty.", argName); + } + } + + private static void ArgumentIsNotNullOrEmpty(string?[]? arg, string argName) + { + if (arg == null) { - ArgumentIsNotNullOrEmpty(to, nameof(to)); - ArgumentIsNotNullOrEmpty(subject, nameof(subject)); - ArgumentIsNotNullOrEmpty(body, nameof(body)); - - From = from; - To = to; - Cc = cc; - Bcc = bcc; - ReplyTo = replyTo; - Subject = subject; - Body = body; - IsBodyHtml = isBodyHtml; - Attachments = attachments?.ToList(); + 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 bbb24b69f7f6..96c52ef9e751 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; } + Stream = stream; + FileName = fileName; + } - public string FileName { get; } + public Stream Stream { get; } - public EmailMessageAttachment(Stream stream, string fileName) - { - Stream = stream; - FileName = fileName; - } - } + public string FileName { get; } } diff --git a/src/Umbraco.Core/Models/Email/NotificationEmailAddress.cs b/src/Umbraco.Core/Models/Email/NotificationEmailAddress.cs index 755947c6a424..c9488f0798de 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; } + Address = address; + DisplayName = displayName; + } - public string Address { get; } + public string DisplayName { get; } - public NotificationEmailAddress(string address, string displayName) - { - Address = address; - DisplayName = displayName; - } - } + public string Address { get; } } diff --git a/src/Umbraco.Core/Models/Email/NotificationEmailModel.cs b/src/Umbraco.Core/Models/Email/NotificationEmailModel.cs index c71519d83f5c..abfea360d919 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; } + From = from; + To = to; + Cc = cc; + Bcc = bcc; + ReplyTo = replyTo; + Subject = subject; + Body = body; + IsBodyHtml = isBodyHtml; + Attachments = attachments?.ToList(); + } - public IEnumerable? Cc { get; } + public NotificationEmailAddress? From { get; } - public IEnumerable? Bcc { get; } + public IEnumerable? To { get; } - public IEnumerable? ReplyTo { get; } + public IEnumerable? Cc { get; } - public string? Subject { get; } + public IEnumerable? Bcc { get; } - public string? Body { get; } + public IEnumerable? ReplyTo { get; } - public bool IsBodyHtml { get; } + public string? Subject { get; } - public IList? Attachments { get; } + public string? Body { get; } - public bool HasAttachments => Attachments != null && Attachments.Count > 0; + public bool IsBodyHtml { get; } - 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(); - } + 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 7b078b35b880..0ae2a142ed52 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 c63ee54a6dbe..887477c743ea 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 -{ - /// - /// Provides a base implementation of and . - /// - [Serializable] - [DataContract(IsReference = true)] - public abstract class BeingDirtyBase : IRememberBeingDirty - { - 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? +namespace Umbraco.Cms.Core.Models.Entities; - #region ICanBeDirty +/// +/// 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? - /// - public virtual bool IsDirty() - { - return _currentChanges != null && _currentChanges.Any(); - } + #region ICanBeDirty - /// - public virtual bool IsPropertyDirty(string propertyName) - { - return _currentChanges != null && _currentChanges.ContainsKey(propertyName); - } + /// + public virtual bool IsDirty() => _currentChanges != null && _currentChanges.Any(); - /// - public virtual IEnumerable GetDirtyProperties() - { - // ReSharper disable once MergeConditionalExpression - return _currentChanges == null - ? Enumerable.Empty() - : _currentChanges.Where(x => x.Value).Select(x => x.Key); - } + /// + public virtual bool IsPropertyDirty(string propertyName) => + _currentChanges != null && _currentChanges.ContainsKey(propertyName); - /// - /// Saves dirty properties so they can be checked with WasDirty. - public virtual void ResetDirtyProperties() - { - ResetDirtyProperties(true); - } + /// + public virtual IEnumerable GetDirtyProperties() => - #endregion + // ReSharper disable once MergeConditionalExpression + _currentChanges == null + ? Enumerable.Empty() + : _currentChanges.Where(x => x.Value).Select(x => x.Key); - #region IRememberBeingDirty + /// + /// Saves dirty properties so they can be checked with WasDirty. + public virtual void ResetDirtyProperties() => ResetDirtyProperties(true); - /// - public virtual bool WasDirty() - { - return _savedChanges != null && _savedChanges.Any(); - } + #endregion - /// - public virtual bool WasPropertyDirty(string propertyName) - { - return _savedChanges != null && _savedChanges.ContainsKey(propertyName); - } + #region IRememberBeingDirty - /// - 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 bool WasDirty() => _savedChanges != null && _savedChanges.Any(); - /// - 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 bool WasPropertyDirty(string propertyName) => + _savedChanges != null && _savedChanges.ContainsKey(propertyName); - /// - public virtual IEnumerable GetWereDirtyProperties() - { - // ReSharper disable once MergeConditionalExpression - return _savedChanges == null - ? Enumerable.Empty() - : _savedChanges.Where(x => x.Value).Select(x => x.Key); - } + /// + public virtual void ResetWereDirtyProperties() => - #endregion + // 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; - #region Change Tracking + /// + 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; + } - /// - /// Occurs when a property changes. - /// - public event PropertyChangedEventHandler? PropertyChanged; + /// + public virtual IEnumerable GetWereDirtyProperties() => - /// - /// Registers that a property has changed. - /// - protected virtual void OnPropertyChanged(string propertyName) - { - if (_withChanges == false) - return; + // ReSharper disable once MergeConditionalExpression + _savedChanges == null + ? Enumerable.Empty() + : _savedChanges.Where(x => x.Value).Select(x => x.Key); - if (_currentChanges == null) - _currentChanges = new Dictionary(); + #endregion - _currentChanges[propertyName] = true; + #region Change Tracking - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } + /// + /// Occurs when a property changes. + /// + public event PropertyChangedEventHandler? PropertyChanged; - /// - /// Disables change tracking. - /// - public void DisableChangeTracking() + /// + /// Registers that a property has changed. + /// + protected virtual void OnPropertyChanged(string propertyName) + { + if (_withChanges == false) { - _withChanges = false; + return; } - /// - /// Enables change tracking. - /// - public void EnableChangeTracking() + if (_currentChanges == null) { - _withChanges = true; + _currentChanges = new Dictionary(); } - /// - /// 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) + _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 (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; + comparer = EqualityComparer.Default; + } - // assign the new value - valueRef = value; + // compare values + var changed = _withChanges && comparer.Equals(valueRef, value) == false; - // handle change - if (changed) - OnPropertyChanged(propertyName); - } + // assign the new value + valueRef = value; - /// - /// 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) + // 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 74bd4e4f44a1..3b9d139ba75a 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 3bc410fc9b9f..a5c0ca23c990 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 static readonly IReadOnlyDictionary Empty = new Dictionary(); - - private IReadOnlyDictionary? _cultureNames; - private IEnumerable? _publishedCultures; - private IEnumerable? _editedCultures; - - /// - public IReadOnlyDictionary CultureNames - { - get => _cultureNames ?? Empty; - set => _cultureNames = value; - } + private IReadOnlyDictionary? _cultureNames; + private IEnumerable? _editedCultures; + private IEnumerable? _publishedCultures; - /// - public IEnumerable PublishedCultures - { - get => _publishedCultures ?? Enumerable.Empty(); - set => _publishedCultures = value; - } + /// + public IReadOnlyDictionary CultureNames + { + get => _cultureNames ?? Empty; + set => _cultureNames = value; + } - /// - public IEnumerable EditedCultures - { - get => _editedCultures ?? Enumerable.Empty(); - set => _editedCultures = value; - } + /// + public IEnumerable PublishedCultures + { + get => _publishedCultures ?? Enumerable.Empty(); + set => _publishedCultures = value; + } - public ContentVariation Variations { get; set; } + /// + public IEnumerable EditedCultures + { + get => _editedCultures ?? Enumerable.Empty(); + set => _editedCultures = value; + } - /// - public bool Published { get; set; } + public ContentVariation Variations { get; set; } - /// - public bool Edited { 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 57b9eeae1fe7..df60d97a1e21 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; - } + SetPropertyValueAndDetectChanges(value, ref _id, nameof(Id)); + _hasIdentity = value != 0; } + } - /// - [DataMember] - public Guid Key + /// + [DataMember] + public Guid Key + { + get { - get + // if an entity does NOT have a key yet, assign one now + if (_key == Guid.Empty) { - // if an entity does NOT have a key yet, assign one now - if (_key == Guid.Empty) - _key = Guid.NewGuid(); - return _key; + _key = Guid.NewGuid(); } - set => SetPropertyValueAndDetectChanges(value, ref _key, nameof(Key)); - } - /// - [DataMember] - public DateTime CreateDate - { - get => _createDate; - set => SetPropertyValueAndDetectChanges(value, ref _createDate, nameof(CreateDate)); + return _key; } + set => SetPropertyValueAndDetectChanges(value, ref _key, nameof(Key)); + } - /// - [DataMember] - public DateTime UpdateDate - { - get => _updateDate; - set => SetPropertyValueAndDetectChanges(value, ref _updateDate, nameof(UpdateDate)); - } + /// + [DataMember] + public DateTime CreateDate + { + get => _createDate; + set => SetPropertyValueAndDetectChanges(value, ref _createDate, nameof(CreateDate)); + } - /// - [DataMember] - public DateTime? DeleteDate { get; set; } // no change tracking - not persisted + /// + [DataMember] + public DateTime UpdateDate + { + get => _updateDate; + set => SetPropertyValueAndDetectChanges(value, ref _updateDate, nameof(UpdateDate)); + } - /// - [DataMember] - public virtual bool HasIdentity => _hasIdentity; + /// + [DataMember] + public DateTime? DeleteDate { get; set; } // no change tracking - not persisted - /// - /// Resets the entity identity. - /// - public virtual void ResetIdentity() - { - _id = default; - _key = Guid.Empty; - _hasIdentity = false; - } + /// + [DataMember] + public virtual bool HasIdentity => _hasIdentity; - public virtual bool Equals(EntityBase? other) - { - return other != null && (ReferenceEquals(this, other) || SameIdentityAs(other)); - } + /// + /// 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) + public override bool Equals(object? obj) => + obj != null && (ReferenceEquals(this, obj) || SameIdentityAs(obj as EntityBase)); + + public override int GetHashCode() + { + unchecked { - return obj != null && (ReferenceEquals(this, obj) || SameIdentityAs(obj as EntityBase)); + var hashCode = HasIdentity.GetHashCode(); + hashCode = (hashCode * 397) ^ Id; + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; } + } - private bool SameIdentityAs(EntityBase? other) + private bool SameIdentityAs(EntityBase? other) + { + if (other == null) { - 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; + return false; } - public override int GetHashCode() + // same identity if + // - same object (reference equals) + // - or same CLR type, both have identities, and they are identical + if (ReferenceEquals(this, other)) { - unchecked - { - var hashCode = HasIdentity.GetHashCode(); - hashCode = (hashCode * 397) ^ Id; - hashCode = (hashCode * 397) ^ GetType().GetHashCode(); - return hashCode; - } + return true; } - 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(); + return GetType() == other.GetType() && HasIdentity && other.HasIdentity && Id == other.Id; + } + + 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 ba3421349d46..53801875aebe 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) + { + 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; + } + } + + /// + /// 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) { - 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) + if (entity.UpdateDate == default || canBeDirty?.IsPropertyDirty("UpdateDate") == false) { - var 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; - } + entity.UpdateDate = now; } } } diff --git a/src/Umbraco.Core/Models/Entities/EntitySlim.cs b/src/Umbraco.Core/Models/Entities/EntitySlim.cs index c4bc473661d3..91acaea3fd0f 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 - { - 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; } + public static readonly IEntitySlim Root = new EntitySlim { Path = "-1", Name = "root", HasChildren = true }; - /// - [DataMember] - public DateTime CreateDate { get; set; } + private IDictionary? _additionalData; - /// - [DataMember] - public DateTime UpdateDate { get; set; } + // implement IEntity - /// - [DataMember] - public DateTime? DeleteDate { get; set; } + /// + [DataMember] + public int Id { get; set; } - /// - [DataMember] - public bool HasIdentity => Id != 0; + /// + [DataMember] + public Guid Key { get; set; } + /// + [DataMember] + public DateTime CreateDate { get; set; } - // implement ITreeEntity + /// + [DataMember] + public DateTime UpdateDate { get; set; } - /// - [DataMember] - public string? Name { get; set; } + /// + [DataMember] + public DateTime? DeleteDate { get; set; } - /// - [DataMember] - public int CreatorId { get; set; } + /// + [DataMember] + public bool HasIdentity => Id != 0; - /// - [DataMember] - public int ParentId { get; set; } + // implement ITreeEntity - /// - public void SetParent(ITreeEntity? parent) => throw new InvalidOperationException("This property won't be implemented."); + /// + [DataMember] + public string? Name { get; set; } - /// - [DataMember] - public int Level { get; set; } + /// + [DataMember] + public int CreatorId { get; set; } - /// - [DataMember] - public string Path { get; set; } = string.Empty; + /// + [DataMember] + public int ParentId { get; set; } - /// - [DataMember] - public int SortOrder { get; set; } + /// + [DataMember] + public int Level { get; set; } - /// - [DataMember] - public bool Trashed { get; set; } + /// + public void SetParent(ITreeEntity? parent) => + throw new InvalidOperationException("This property won't be implemented."); + /// + [DataMember] + public string Path { get; set; } = string.Empty; - // implement IUmbracoEntity + /// + [DataMember] + public int SortOrder { get; set; } - /// - [DataMember] - public IDictionary? AdditionalData => _additionalData ?? (_additionalData = new Dictionary()); + /// + [DataMember] + public bool Trashed { get; set; } - /// - [IgnoreDataMember] - public bool HasAdditionalData => _additionalData != null; + // implement IUmbracoEntity + /// + [DataMember] + public IDictionary? AdditionalData => +_additionalData ??= new Dictionary(); - // implement IEntitySlim + /// + [IgnoreDataMember] + public bool HasAdditionalData => _additionalData != null; - /// - [DataMember] - public Guid NodeObjectType { get; set; } + // implement IEntitySlim - /// - [DataMember] - public bool HasChildren { get; set; } + /// + [DataMember] + public Guid NodeObjectType { get; set; } - /// - [DataMember] - public virtual bool IsContainer { get; set; } + /// + [DataMember] + public bool HasChildren { get; set; } + /// + [DataMember] + public virtual bool IsContainer { get; set; } - #region IDeepCloneable + #region IDeepCloneable - /// - public object DeepClone() - { - throw new InvalidOperationException("This method won't be implemented."); - } + /// + public object DeepClone() => throw new InvalidOperationException("This method won't be implemented."); - #endregion + #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 void ResetIdentity() + { + Id = default; + Key = Guid.Empty; + } - public bool IsDirty() - { - throw new InvalidOperationException("This method won't be implemented."); - } + #region IRememberBeingDirty - public bool IsPropertyDirty(string propName) - { - throw new InvalidOperationException("This method won't be implemented."); - } + // 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 IEnumerable GetDirtyProperties() - { - throw new InvalidOperationException("This method won't be implemented."); - } + public bool IsPropertyDirty(string propName) => + throw new InvalidOperationException("This method won't be implemented."); - public void ResetDirtyProperties() - { - throw new InvalidOperationException("This method won't be implemented."); - } + public IEnumerable GetDirtyProperties() => + throw new InvalidOperationException("This method won't be implemented."); - public bool WasDirty() - { - throw new InvalidOperationException("This method won't be implemented."); - } + public void ResetDirtyProperties() => throw new InvalidOperationException("This method won't be implemented."); - public bool WasPropertyDirty(string propertyName) - { - throw new InvalidOperationException("This method won't be implemented."); - } + public bool WasDirty() => throw new InvalidOperationException("This method won't be implemented."); - public void ResetWereDirtyProperties() - { - 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 ResetDirtyProperties(bool rememberDirty) - { - throw new InvalidOperationException("This method won't be implemented."); - } + public void ResetWereDirtyProperties() => throw new InvalidOperationException("This method won't be implemented."); - public IEnumerable GetWereDirtyProperties() - { - throw new InvalidOperationException("This method won't be implemented."); - } + public void ResetDirtyProperties(bool rememberDirty) => + throw new InvalidOperationException("This method won't be implemented."); - #endregion + 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 d8644431d526..23d50d54d918 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; + + /// + /// Determines whether the current entity is dirty. + /// + bool IsDirty(); + + /// + /// Determines whether a specific property is dirty. + /// + bool IsPropertyDirty(string propName); + + /// + /// Gets properties that are dirty. + /// + IEnumerable GetDirtyProperties(); + + /// + /// Resets dirty properties. + /// + void ResetDirtyProperties(); + + /// + /// Disables change tracking. + /// + void DisableChangeTracking(); + /// - /// Defines an entity that tracks property changes and can be dirty. + /// Enables change tracking. /// - public interface ICanBeDirty - { - /// - /// Determines whether the current entity is dirty. - /// - bool IsDirty(); - - /// - /// Determines whether a specific property is dirty. - /// - bool IsPropertyDirty(string propName); - - /// - /// Gets properties that are dirty. - /// - IEnumerable GetDirtyProperties(); - - /// - /// Resets dirty properties. - /// - void ResetDirtyProperties(); - - /// - /// Disables change tracking. - /// - void DisableChangeTracking(); - - /// - /// Enables change tracking. - /// - void EnableChangeTracking(); - - event PropertyChangedEventHandler PropertyChanged; - } + void EnableChangeTracking(); } diff --git a/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs index 52ea701af3a6..78ddf9bd8269 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 d160e144bb7e..75e16476c25d 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 { - /// - /// Represents a lightweight document entity, managed by the entity service. + /// Gets the variant name for each culture /// - public interface IDocumentEntitySlim : IContentEntitySlim - { - /// - /// Gets the variant name for each culture - /// - IReadOnlyDictionary CultureNames { get; } - - /// - /// Gets the published cultures. - /// - IEnumerable PublishedCultures { get; } + IReadOnlyDictionary CultureNames { get; } - /// - /// Gets the edited cultures. - /// - IEnumerable EditedCultures { get; } + /// + /// Gets the published cultures. + /// + IEnumerable PublishedCultures { get; } - /// - /// Gets the content variation of the content type. - /// - ContentVariation Variations { get; } + /// + /// Gets the edited cultures. + /// + IEnumerable EditedCultures { get; } - /// - /// Gets a value indicating whether the content is published. - /// - bool Published { get; } + /// + /// Gets the content variation of the content type. + /// + ContentVariation Variations { get; } - /// - /// Gets a value indicating whether the content has been edited. - /// - bool Edited { 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; } } diff --git a/src/Umbraco.Core/Models/Entities/IEntity.cs b/src/Umbraco.Core/Models/Entities/IEntity.cs index 6aeea5855392..859975adfb9d 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 dfdb00edaa7d..120d417d1ad0 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 651e6a5f7a93..a2ac3a247ac3 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 3a2996c6fee2..019a6f1f7b77 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 a43607fda77c..0ded5370355f 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 618bab2698d9..85c1c472b5cd 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 af105d63ff7f..b66368e425ca 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. - /// - public interface ITreeEntity : IEntity - { - /// - /// Gets or sets the name of the entity. - /// - 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 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 path to the entity. - /// - string Path { 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 or sets the name of the entity. + /// + 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 parent entity. + /// + int ParentId { 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 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; } + + /// + /// 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 d89e5d93125b..3d8c89c7c4b0 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 e1b7ea01a628..f101f531fa13 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 fd3c01e15c42..fb73e2332dd1 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 66e3650fc51b..923fef2477e7 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 b5d6f40a4c9f..f10e49b9574e 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; - - /// - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(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 + { + get => _creatorId; + set => SetPropertyValueAndDetectChanges(value, ref _creatorId, nameof(CreatorId)); + } - /// - [DataMember] - public int ParentId + /// + [DataMember] + public int ParentId + { + get { - 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."); + return _parentId; + } - _parentId = _parent.Id; - if (_parentId == 0) - throw new Exception("Panic: parent has an identity but id is zero."); + if (_parent == null) + { + throw new InvalidOperationException("Content does not have a parent."); + } - _hasParentId = true; - _parent = null; - return _parentId; + if (!_parent.HasIdentity) + { + throw new InvalidOperationException("Content's parent does not have an identity."); } - set + + _parentId = _parent.Id; + if (_parentId == 0) { - if (value == 0) - throw new ArgumentException("Value cannot be zero.", nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); - _hasParentId = true; - _parent = null; + throw new Exception("Panic: parent has an identity but id is zero."); } - } - /// - public void SetParent(ITreeEntity? parent) - { - _hasParentId = false; - _parent = parent; - OnPropertyChanged(nameof(ParentId)); + _hasParentId = true; + _parent = null; + return _parentId; } - /// - [DataMember] - public int Level + set { - get => _level; - set => SetPropertyValueAndDetectChanges(value, ref _level, nameof(Level)); - } + if (value == 0) + { + throw new ArgumentException("Value cannot be zero.", nameof(value)); + } - /// - [DataMember] - public string Path - { - get => _path; - set => SetPropertyValueAndDetectChanges(value, ref _path!, nameof(Path)); + SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); + _hasParentId = true; + _parent = null; } + } - /// - [DataMember] - public int SortOrder - { - get => _sortOrder; - set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); - } + /// + [DataMember] + public int Level + { + get => _level; + set => SetPropertyValueAndDetectChanges(value, ref _level, nameof(Level)); + } - /// - [DataMember] - public bool Trashed - { - get => _trashed; - set => SetPropertyValueAndDetectChanges(value, ref _trashed, nameof(Trashed)); - } + /// + 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 6fd147ace769..fe284a1e1119 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 114d78605c24..762297af079f 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 { + private static readonly Dictionary ObjectTypeMap = new() + { + { Constants.ObjectTypes.DataType, Constants.ObjectTypes.DataTypeContainer }, + { Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentTypeContainer }, + { Constants.ObjectTypes.MediaType, Constants.ObjectTypes.MediaTypeContainer }, + }; + /// - /// Represents a folder for organizing entities such as content types and data types. + /// Initializes a new instance of an class. /// - public sealed class EntityContainer : TreeEntityBase, IUmbracoEntity + public EntityContainer(Guid containedObjectType) { - private readonly Guid _containedObjectType; - - private static readonly Dictionary ObjectTypeMap = new Dictionary + if (ObjectTypeMap.ContainsKey(containedObjectType) == false) { - { Constants.ObjectTypes.DataType, Constants.ObjectTypes.DataTypeContainer }, - { Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentTypeContainer }, - { Constants.ObjectTypes.MediaType, Constants.ObjectTypes.MediaTypeContainer } - }; + throw new ArgumentException("Not a contained object type.", nameof(containedObjectType)); + } - /// - /// 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; + ContainedObjectType = containedObjectType; - ParentId = -1; - Path = "-1"; - Level = 0; - SortOrder = 0; - } + 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; - } + /// + /// 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 => _containedObjectType; + /// + /// 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 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 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) { - if (ObjectTypeMap.ContainsKey(containedObjectType) == false) - throw new ArgumentException("Not a contained object type.", nameof(containedObjectType)); - return ObjectTypeMap[containedObjectType]; + throw new ArgumentException("Not a contained object type.", nameof(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) + return ObjectTypeMap[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) { - var contained = ObjectTypeMap.FirstOrDefault(x => x.Value == containerObjectType).Key; - if (contained == null) - throw new ArgumentException("Not a container object type.", nameof(containerObjectType)); - return contained; + throw new ArgumentException("Not a container object type.", nameof(containerObjectType)); } + + return contained; } } diff --git a/src/Umbraco.Core/Models/File.cs b/src/Umbraco.Core/Models/File.cs index 3865d4eee79d..8abfdd1ef559 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 -{ - /// - /// 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 _path; - private string _originalPath; +namespace Umbraco.Cms.Core.Models; - // 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; } - - protected File(string path, Func? getFileContent = null) - { - _path = SanitizePath(path); - _originalPath = _path; - GetFileContent = getFileContent; - _content = getFileContent != null ? null : string.Empty; - } +/// +/// 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; - private string? _alias; - private string? _name; + // 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; - private static string SanitizePath(string path) - { - return path - .Replace('\\', System.IO.Path.DirectorySeparatorChar) - .Replace('/', System.IO.Path.DirectorySeparatorChar); + protected File(string path, Func? getFileContent = null) + { + _path = SanitizePath(path); + OriginalPath = _path; + GetFileContent = getFileContent; + _content = getFileContent != null ? null : string.Empty; + } - //Don't strip the start - this was a bug fixed in 7.3, see ScriptRepositoryTests.PathTests - //.TrimStart(System.IO.Path.DirectorySeparatorChar) - //.TrimStart('/'); - } + public Func? GetFileContent { get; set; } - /// - /// 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 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 + /// + /// Gets or sets the Alias of the File, which is the name without the extension + /// + [DataMember] + public virtual string Alias + { + get { - 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; - } - } - /// - /// 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)); + var lastIndexOf = name.LastIndexOf(".", StringComparison.InvariantCultureIgnoreCase); + _alias = name.Substring(0, lastIndexOf); } - } - /// - /// Gets the original path of the file - /// - public string OriginalPath - { - get { return _originalPath; } + return _alias; } + } - /// - /// Called to re-set the OriginalPath to the Path - /// - public void ResetOriginalPath() + /// + /// 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 { - _originalPath = _path; + // 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 + /// + /// 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 { - get + if (_content != null) { - 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); + return _content; } - set + + // else, must lazy-load, and ensure it's not null + if (GetFileContent != null) { - SetPropertyValueAndDetectChanges( - value ?? string.Empty, // cannot set to null - ref _content, nameof(Content)); + _content = GetFileContent(this); } + + return _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; } + /// + /// Called to re-set the OriginalPath to the Path + /// + public void ResetOriginalPath() => OriginalPath = _path; - // 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; - } + /// + /// 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; + } - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); + private static string SanitizePath(string path) => + path + .Replace('\\', System.IO.Path.DirectorySeparatorChar) + .Replace('/', System.IO.Path.DirectorySeparatorChar); - var clonedFile = (File)clone; + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); - // clear fields that were memberwise-cloned and that we don't want to clone - clonedFile._content = null; + var clonedFile = (File)clone; - // ... - DeepCloneNameAndAlias(clonedFile); - } + // 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 810bcaf3b3de..60e636ca6e50 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 1c1c3774033f..79db47414a2b 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 e12237f06d0e..3a1b412ce0da 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. - /// - /// - /// 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; } - - /// - /// 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 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 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 free-form details about the audited event. - /// - string? EventDetails { get; set; } - } + /// Gets or sets the identifier of the user triggering the audited event. + /// + int PerformingUserId { 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 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 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 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 dbc7ad1fd4ad..dbf4fe01e881 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 747e7a145c74..bae029428301 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; } - - /// - /// Gets the unique identifier of whoever is consenting. - /// - string? Source { 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 unique identifier of the consented action. - /// - string? Action { 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; } - } + string? Context { get; } + + /// + /// Gets the unique identifier of the consented action. + /// + string? Action { 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; } } diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index e538b307e2d6..9e36306cfc78 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; } + + /// + /// 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; } /// - /// Represents a document. + /// 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; } - - /// - /// 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. - /// - /// Will return `true` once unpublished edits have been made after the version with has been published. - bool Edited { get; set; } - - /// - /// Gets the version identifier for the currently published version of the content. - /// - int PublishedVersionId { get; set; } - - /// - /// Gets a value indicating whether the content item is a blueprint. - /// - bool Blueprint { 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 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 identifier of the user who published the content. - /// - int? PublisherId { get; set; } - - /// - /// Gets the date and time the content was published. - /// - DateTime? PublishDate { 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 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 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(); - - } + bool Edited { get; set; } + + /// + /// Gets the version identifier for the currently published version of the content. + /// + int PublishedVersionId { get; set; } + + /// + /// Gets a value indicating whether the content item is a blueprint. + /// + bool Blueprint { 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 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 identifier of the user who published the content. + /// + int? PublisherId { get; set; } + + /// + /// Gets the date and time the content was published. + /// + DateTime? PublishDate { 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 published cultures. + /// + IEnumerable PublishedCultures { get; } + + /// + /// 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 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 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); + + /// + /// 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 20e78816ae45..5f4ccf524487 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; } + + /// + /// 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. + /// + /// + /// + /// 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 available cultures. + /// + /// + /// Cannot contain the invariant culture, which is always available. + /// + IEnumerable AvailableCultures { get; } + + /// + /// 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; } + + /// + /// 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); /// - /// Provides a base class for content items. + /// Gets a value indicating whether a given culture is available. /// /// - /// Content items are documents, medias and members. - /// Content items have a content type, and properties. + /// + /// 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). /// - public interface IContentBase : IUmbracoEntity, IRememberBeingDirty - { - /// - /// Integer Id of the default ContentType - /// - int ContentTypeId { get; } - - /// - /// 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; } - - /// - /// 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 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 available cultures. - /// - /// - /// Cannot contain the invariant culture, which is always available. - /// - IEnumerable AvailableCultures { get; } - - /// - /// 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 date a culture was updated. - /// - /// - /// When is null, returns null. - /// If the specified culture is not available, returns null. - /// - DateTime? GetUpdateDate(string culture); - - /// - /// 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); - - } + bool IsCultureAvailable(string culture); + + /// + /// 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 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); } diff --git a/src/Umbraco.Core/Models/IContentModel.cs b/src/Umbraco.Core/Models/IContentModel.cs index 8aa8c1830676..c7669dfbe4f1 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 e3fd83fcd340..5d76c49b88a1 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 +{ /// - /// Defines a ContentType, which Content is based on + /// Internal property to store the Id of the default template /// - public interface IContentType : IContentTypeComposition - { - /// - /// Internal property to store the Id of the default template - /// - int DefaultTemplateId { get; set; } + int DefaultTemplateId { get; set; } - /// - /// Gets the default Template of the ContentType - /// - 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 eb3d4489d4e9..adcb4074f9ab 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. - /// - public interface IContentTypeBase : IUmbracoEntity, IRememberBeingDirty - { - /// - /// Gets or Sets the Alias of the ContentType - /// - string Alias { 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 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 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 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); - - /// - /// 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 a list of integer Ids of the ContentTypes allowed under the ContentType - /// - IEnumerable? AllowedContentTypes { get; set; } - - /// - /// Gets or sets the local property groups. - /// - PropertyGroupCollection PropertyGroups { get; set; } - - /// - /// Gets all local property types all local property groups or ungrouped. - /// - IEnumerable PropertyTypes { get; } - - /// - /// Gets or sets the local property types that do not belong to a group. - /// - IEnumerable NoGroupPropertyTypes { get; set; } - - /// - /// 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); - - /// - /// 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 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); - - /// - /// 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 or Sets the Alias of the ContentType + /// + string Alias { 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 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 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 the content variation of the content type. + /// + ContentVariation Variations { get; set; } + + /// + /// Gets or Sets a list of integer Ids of the ContentTypes allowed under the ContentType + /// + IEnumerable? AllowedContentTypes { get; set; } + + /// + /// Gets or sets the local property groups. + /// + PropertyGroupCollection PropertyGroups { get; set; } + + /// + /// Gets all local property types all local property groups or ungrouped. + /// + IEnumerable PropertyTypes { get; } + + /// + /// 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. + /// + /// 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); + + /// + /// 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 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); + + /// + /// 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 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); + + /// + /// 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 de3e8fb41647..650328548e3e 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 2fdc67dfcc8a..6f0002c77946 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 8d4841a11479..73b700e411de 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 +{ /// - /// Represents an editor for editing data values. + /// Gets the editor view. /// - /// This is the base interface for parameter and property value editors. - public interface IDataValueEditor - { - /// - /// Gets the editor view. - /// - string? View { get; } + string? View { get; } - /// - /// Gets the type of the value. - /// - /// 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; } - /// - /// 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; } - /// - /// 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); - /// - /// 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 a7568b7e815c..171c2a1f4ef3 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 f299ce2ac5a8..e47502199b2e 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 445bafd4bab7..37579151bc42 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 f9d90dd9eb4b..2d4845c9a6f8 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; } + + string DomainName { get; set; } + + int? RootContentId { get; set; } + + bool IsWildcard { get; } - /// - /// Readonly value of the language ISO code for the domain - /// - string? LanguageIsoCode { 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 216d45d2772d..ed52997c84ea 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 + /// + string? Name { 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 the original path of the file + /// + string OriginalPath { get; } + + /// + /// 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; } + + /// + /// Called to re-set the OriginalPath to the Path /// - /// Used for Scripts, Stylesheets and Templates - public interface IFile : IEntity, IRememberBeingDirty - { - /// - /// Gets the Name of the File including extension - /// - string? Name { 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 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 file's virtual path (i.e. the file path relative to the root of the website) - /// - string? VirtualPath { get; set; } - - } + void ResetOriginalPath(); } diff --git a/src/Umbraco.Core/Models/IKeyValue.cs b/src/Umbraco.Core/Models/IKeyValue.cs index 2a8c6528bf2c..b893aabf356e 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 de5170cff651..5f48bc363eb3 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -2,56 +2,59 @@ 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 59a567a6359b..372fddc3d02c 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 e8102b7768b4..bc979804a753 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 d3d589a31e8a..e1b27b64839c 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 { - /// - /// Defines a Property for a Macro - /// - public interface IMacroProperty : IValueObject, IDeepCloneable, IRememberBeingDirty - { - [DataMember] - int Id { get; set; } + [DataMember] + int Id { get; set; } - [DataMember] - Guid Key { get; set; } + [DataMember] + Guid Key { get; set; } - /// - /// Gets or sets the Alias of the Property - /// - [DataMember] - string Alias { get; set; } + /// + /// Gets or sets the Alias of the Property + /// + [DataMember] + string Alias { get; set; } - /// - /// Gets or sets the Name of the Property - /// - [DataMember] - string? Name { 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 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 cbb80fdd5967..08f206f664e9 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 13655f0f55dc..0be980ae6219 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 4565117dfd3a..a0af9dcc0e20 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 0dba1d804984..6085b84f01b7 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 +namespace Umbraco.Cms.Core.Models; + +public interface IMember : IContentBase, IMembershipUser, IHaveAdditionalData { - public interface IMember : IContentBase, IMembershipUser, IHaveAdditionalData - { - /// - /// String alias of the default ContentType - /// - string ContentTypeAlias { get; } + /// + /// 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? 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; } - } + /// + /// 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 80d4a16ad6a2..904d60cf8c65 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 324601efde11..993e956df986 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 a3d11e851a76..392eb17097c8 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 c45b76534de1..a19cc65c7a78 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 9ed37c34e1ad..54f1e8581fc4 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; } - /// - /// Returns the PropertyType, which this Property is based on - /// - IPropertyType PropertyType { get; } - - /// - /// Gets the list of values. - /// - IReadOnlyCollection Values { get; set; } - - /// - /// 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); - - /// - /// Sets a value. - /// - void SetValue(object? value, string? culture = null, string? segment = null); - - int PropertyTypeId { get; } - void PublishValues(string? culture = "*", string segment = "*"); - void UnpublishValues(string? culture = "*", string segment = "*"); - - } + ValueStorageType ValueStorageType { get; } + + /// + /// Returns the PropertyType, which this Property is based on + /// + IPropertyType PropertyType { get; } + + /// + /// Gets the list of values. + /// + IReadOnlyCollection Values { get; set; } + + /// + /// Returns the Alias of the PropertyType, which this Property is based on + /// + string Alias { get; } + + int PropertyTypeId { get; } + + /// + /// Gets the value. + /// + object? GetValue(string? culture = null, string? segment = null, bool published = false); + + /// + /// 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 d39a214fdd97..535756fad86f 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); - - /// - /// Ensures that the collection contains properties for the specified property types. - /// - void EnsurePropertyTypes(IEnumerable propertyTypes); - - /// - /// Ensures that the collection does not contain properties not in the specified property types. - /// - void EnsureCleanPropertyTypes(IEnumerable propertyTypes); - - /// - /// Gets the property with the specified alias. - /// - IProperty? this[string name] { get; } - - /// - /// Gets the property at the specified index. - /// - IProperty? this[int index] { get; } - - /// - /// Adds or updates a property. - /// - void Add(IProperty property); - - int Count { get; } - void ClearCollectionChangedEvents(); - } + int Count { get; } + + /// + /// Gets the property with the specified alias. + /// + IProperty? this[string name] { get; } + + /// + /// Gets the property at the specified index. + /// + IProperty? this[int index] { get; } + + bool TryGetValue(string propertyTypeAlias, [MaybeNullWhen(false)] out IProperty property); + + bool Contains(string key); + + /// + /// Ensures that the collection contains properties for the specified property types. + /// + void EnsurePropertyTypes(IEnumerable propertyTypes); + + /// + /// 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 b820c1d7aabb..a48f8e01ae74 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 alias of the property type. - /// - string Alias { 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; } - - 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 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 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 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; } - - bool SupportsPublishing { 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 pattern for this PropertyType must be matched - /// - string? ValidationRegExpMessage { 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 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; } + + 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 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 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 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; } + + bool SupportsPublishing { get; set; } + + /// + /// Gets or sets the content variation of the property type. + /// + ContentVariation Variations { 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; } + + /// + /// 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 77e9e1dc2568..ef95cd2a01fa 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 f7518140f531..37b5a5ddea5e 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 18498837b416..cbd12eb0b88d 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; } - - /// - /// Gets or sets the unique key identifying the content item. - /// - [DataMember] - Guid ContentKey { get; set; } + [DataMember] + int ContentId { get; set; } - /// - /// Gets or sets the redirect URL creation date. - /// - [DataMember] - DateTime CreateDateUtc { get; set; } + /// + /// Gets or sets the unique key identifying the content item. + /// + [DataMember] + Guid ContentKey { get; set; } - /// - /// Gets or sets the culture. - /// - [DataMember] - string? Culture { get; set; } + /// + /// Gets or sets the redirect URL creation date. + /// + [DataMember] + DateTime CreateDateUtc { 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 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; } } diff --git a/src/Umbraco.Core/Models/IRelation.cs b/src/Umbraco.Core/Models/IRelation.cs index 0370bbe61f2f..468a51b89706 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; } - - [DataMember] - Guid ParentObjectType { get; set; } - - /// - /// Gets or sets the Child Id of the Relation (Destination) - /// - [DataMember] - int ChildId { get; set; } - - [DataMember] - Guid ChildObjectType { 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 the Id of the that this Relation is based on. - /// - [IgnoreDataMember] - int RelationTypeId { get; } - } + /// + /// Gets or sets the Parent Id of the Relation (Source) + /// + [DataMember] + int ParentId { get; set; } + + [DataMember] + Guid ParentObjectType { get; set; } + + /// + /// Gets or sets the Child Id of the Relation (Destination) + /// + [DataMember] + int ChildId { get; set; } + + [DataMember] + Guid ChildObjectType { 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 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 cbc485f64bfe..7675a1c49e65 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; } - } + /// + /// 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; } +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 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 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 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 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 6a07d2aa2540..f52bdc02864a 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 7d8c0f58c101..525ed30163e2 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 503946ba9630..8246b50ca01b 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. - /// - public interface ISimpleContentType - { - int Id { get; } - Guid Key { get; } - string? Name { get; } - - /// - /// Gets the alias of the content type. - /// - string Alias { 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 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 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; } - - /// - /// 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 the alias of the content type. + /// + string Alias { 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 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 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; } + + /// + /// 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 e7710f26df3b..fbe9a1652b71 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 781fb474b2ee..c2bb81060da7 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 79840481bb2e..9824ee5ed2e7 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 e20dcc55fa0b..321fff2831e3 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 ca005309b26c..3840dcb1746c 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 6b09c0860225..8fd9005ac3ce 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 118f7348aed6..68544289c605 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 1cd7294a58df..3ce2f4bfb94b 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 876b2bfddb7e..9fd00ac2abfd 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 ImageUrlGenerationOptions(string? imageUrl) => ImageUrl = imageUrl; - public string? ImageUrl { get; } + public string? ImageUrl { get; } - public int? Width { get; set; } + public int? Width { get; set; } - public int? Height { get; set; } + public int? Height { get; set; } - public int? Quality { get; set; } + public int? Quality { get; set; } - public ImageCropMode? ImageCropMode { get; set; } + public ImageCropMode? ImageCropMode { get; set; } - public ImageCropAnchor? ImageCropAnchor { get; set; } + public ImageCropAnchor? ImageCropAnchor { get; set; } - public FocalPointPosition? FocalPoint { get; set; } + public FocalPointPosition? FocalPoint { get; set; } - public CropCoordinates? Crop { get; set; } + public CropCoordinates? Crop { get; set; } - public string? CacheBusterValue { get; set; } + public string? CacheBusterValue { get; set; } - public string? FurtherOptions { get; set; } + public string? FurtherOptions { get; set; } - public override bool Equals(object? obj) => Equals(obj as ImageUrlGenerationOptions); + 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 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 int GetHashCode() - { - 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(); - } + public override bool Equals(object? obj) => Equals(obj as ImageUrlGenerationOptions); - /// - /// 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 override int GetHashCode() + { + var hash = default(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(); + } + + /// + /// 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) { - public FocalPointPosition(decimal left, decimal top) - { - Left = left; - Top = top; - } + Left = left; + Top = top; + } - public decimal Left { get; } + public decimal Left { get; } - public decimal Top { get; } + public decimal Top { get; } - public override bool Equals(object? obj) => Equals(obj as FocalPointPosition); + public bool Equals(FocalPointPosition? other) + => other != null && + Left == other.Left && + Top == other.Top; - public bool Equals(FocalPointPosition? other) - => other != null && - Left == other.Left && - Top == other.Top; + public override bool Equals(object? obj) => Equals(obj as FocalPointPosition); - public override int GetHashCode() => HashCode.Combine(Left, Top); - } + 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 + /// + /// 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) { - public CropCoordinates(decimal left, decimal top, decimal right, decimal bottom) - { - Left = left; - Top = top; - Right = right; - Bottom = bottom; - } + Left = left; + Top = top; + Right = right; + Bottom = bottom; + } - public decimal Left { get; } + public decimal Left { get; } - public decimal Top { get; } + public decimal Top { get; } - public decimal Right { get; } + public decimal Right { get; } - public decimal Bottom { 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 bool Equals(CropCoordinates? other) - => other != null && - Left == other.Left && - Top == other.Top && - Right == other.Right && - Bottom == other.Bottom; + public override bool Equals(object? obj) => Equals(obj as CropCoordinates); - 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 4e38ee3390d6..bf5b26dbee58 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 -{ - /// - /// Implements . - /// - [Serializable] - [DataContract(IsReference = true)] - public class KeyValue : EntityBase, IKeyValue, IEntity - { - private string _identifier = null!; - private string? _value; +namespace Umbraco.Cms.Core.Models; - /// - public string Identifier - { - get => _identifier; - set => SetPropertyValueAndDetectChanges(value, ref _identifier!, nameof(Identifier)); - } +/// +/// Implements . +/// +[Serializable] +[DataContract(IsReference = true)] +public class KeyValue : EntityBase, IKeyValue +{ + private string _identifier = null!; + private string? _value; - /// - public string? Value - { - get => _value; - set => SetPropertyValueAndDetectChanges(value, ref _value, nameof(Value)); - } + /// + public string Identifier + { + get => _identifier; + set => SetPropertyValueAndDetectChanges(value, ref _identifier!, nameof(Identifier)); + } - bool IEntity.HasIdentity => !string.IsNullOrEmpty(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 20d936af616c..9299665755c3 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -3,88 +3,88 @@ 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; - - /// - /// Initializes a new instance of the class. - /// - /// The ISO code of the language. - /// The name of the language. - public Language(string isoCode, string cultureName) - { - _isoCode = isoCode ?? throw new ArgumentNullException(nameof(isoCode)); - _cultureName = cultureName ?? throw new ArgumentNullException(nameof(cultureName)); - } + _isoCode = isoCode ?? throw new ArgumentNullException(nameof(isoCode)); + _cultureName = cultureName ?? throw new ArgumentNullException(nameof(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; - } + [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 + /// + [DataMember] + public string IsoCode + { + get => _isoCode; + set { - get => _isoCode; - set - { - ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(value); - SetPropertyValueAndDetectChanges(value, ref _isoCode!, nameof(IsoCode)); - } + SetPropertyValueAndDetectChanges(value, ref _isoCode!, nameof(IsoCode)); } + } - /// - [DataMember] - public string CultureName + /// + [DataMember] + public string CultureName + { + get => _cultureName; + set { - get => _cultureName; - set - { - ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(value); - SetPropertyValueAndDetectChanges(value, ref _cultureName!, nameof(CultureName)); - } + SetPropertyValueAndDetectChanges(value, ref _cultureName!, nameof(CultureName)); } + } - /// - [IgnoreDataMember] - public CultureInfo? CultureInfo => IsoCode is not null ? CultureInfo.GetCultureInfo(IsoCode) : null; + /// + [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 IsDefault + { + get => _isDefaultVariantLanguage; + set => SetPropertyValueAndDetectChanges(value, ref _isDefaultVariantLanguage, nameof(IsDefault)); + } - /// - public bool IsMandatory - { - get => _mandatory; - set => SetPropertyValueAndDetectChanges(value, ref _mandatory, nameof(IsMandatory)); - } + /// + public bool IsMandatory + { + get => _mandatory; + set => SetPropertyValueAndDetectChanges(value, ref _mandatory, nameof(IsMandatory)); + } - /// - public int? FallbackLanguageId - { - get => _fallbackLanguageId; - set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, nameof(FallbackLanguageId)); - } + /// + 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 3bfc9c5a0db0..7047b54555ff 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 e4879249d8b7..500380504374 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 e9c0dc3180da..5941763e24db 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; + private string? _name; + private string? _query; - public LogViewerQuery(string? name, 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 1e395c2158c7..ea03750e32fb 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 -{ - /// - /// Represents a Macro - /// - [Serializable] - [DataContract(IsReference = true)] - public class Macro : EntityBase, IMacro - { - private readonly IShortStringHelper _shortStringHelper; +namespace Umbraco.Cms.Core.Models; - public Macro(IShortStringHelper shortStringHelper) - { - _alias = string.Empty; - _shortStringHelper = shortStringHelper; - _properties = new MacroPropertyCollection(); - _properties.CollectionChanged += PropertiesChanged; - _addedProperties = new List(); - _removedProperties = new List(); - _macroSource = string.Empty; - } +/// +/// Represents a Macro +/// +[Serializable] +[DataContract(IsReference = true)] +public class Macro : EntityBase, IMacro +{ + private readonly IShortStringHelper _shortStringHelper; + private List _addedProperties; - /// - /// 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; - } + 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; - /// - /// 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; - } + public Macro(IShortStringHelper shortStringHelper) + { + _alias = string.Empty; + _shortStringHelper = shortStringHelper; + Properties = new MacroPropertyCollection(); + Properties.CollectionChanged += PropertiesChanged; + _addedProperties = new List(); + _removedProperties = new List(); + _macroSource = string.Empty; + } - 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; + /// + /// 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; + } - void PropertiesChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(Properties)); + /// + /// 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; + } - if (e.Action == NotifyCollectionChangedAction.Add) - { - //listen for changes - MacroProperty? prop = e.NewItems?.Cast().First(); - if (prop is not null) - { - prop.PropertyChanged += PropertyDataChanged; + /// + /// 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)); + } - var alias = prop.Alias; + /// + /// Used internally to check if we need to add a section in the repository to the db + /// + internal IEnumerable AddedProperties => _addedProperties; - 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; + /// + /// Used internally to check if we need to remove a section in the repository to the db + /// + internal IEnumerable RemovedProperties => _removedProperties; - var alias = prop.Alias; + public override void ResetDirtyProperties(bool rememberDirty) + { + base.ResetDirtyProperties(rememberDirty); - if (_removedProperties.Contains(alias) == false) - { - _removedProperties.Add(alias); - } - } - } - } + _addedProperties.Clear(); + _removedProperties.Clear(); - /// - /// When some data of a property has changed ensure our Properties flag is dirty - /// - /// - /// - void PropertyDataChanged(object? sender, PropertyChangedEventArgs e) + foreach (IMacroProperty prop in Properties) { - OnPropertyChanged(nameof(Properties)); + prop.ResetDirtyProperties(rememberDirty); } + } - public override void ResetDirtyProperties(bool rememberDirty) - { - base.ResetDirtyProperties(rememberDirty); + /// + /// Gets or sets the name of the Macro + /// + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } - _addedProperties.Clear(); - _removedProperties.Clear(); + /// + /// 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)); + } - foreach (var prop in Properties) - { - prop.ResetDirtyProperties(rememberDirty); - } - } + /// + /// Gets or sets the Cache Duration for the Macro + /// + [DataMember] + public int CacheDuration + { + get => _cacheDuration; + set => SetPropertyValueAndDetectChanges(value, ref _cacheDuration, nameof(CacheDuration)); + } - /// - /// Used internally to check if we need to add a section in the repository to the db - /// - internal IEnumerable AddedProperties => _addedProperties; + /// + /// 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)); + } - /// - /// Used internally to check if we need to remove a section in the repository to the db - /// - internal IEnumerable RemovedProperties => _removedProperties; + /// + /// 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 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 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 sets the name of the Macro - /// - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } + /// + /// 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 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 a list of Macro Properties + /// + [DataMember] + public MacroPropertyCollection Properties { get; private set; } - /// - /// Gets or sets the Cache Duration for the Macro - /// - [DataMember] - public int CacheDuration - { - get => _cacheDuration; - set => SetPropertyValueAndDetectChanges(value, ref _cacheDuration, nameof(CacheDuration)); - } + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); - /// - /// 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)); - } + var clonedEntity = (Macro)clone; - /// - /// 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)); - } + clonedEntity._addedProperties = new List(); + clonedEntity._removedProperties = new List(); + clonedEntity.Properties = (MacroPropertyCollection)Properties.DeepClone(); - /// - /// 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)); - } + // re-assign the event handler + clonedEntity.Properties.CollectionChanged += clonedEntity.PropertiesChanged; + } - /// - /// Gets or set the path to the Partial View to render - /// - [DataMember] - public string MacroSource + private void PropertiesChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(Properties)); + + if (e.Action == NotifyCollectionChangedAction.Add) { - get => _macroSource; - set => SetPropertyValueAndDetectChanges(value, ref _macroSource!, nameof(MacroSource)); - } + // listen for changes + MacroProperty? prop = e.NewItems?.Cast().First(); + if (prop is not null) + { + prop.PropertyChanged += PropertyDataChanged; - /// - /// Gets or sets a list of Macro Properties - /// - [DataMember] - public MacroPropertyCollection Properties => _properties; + var alias = prop.Alias; - protected override void PerformDeepClone(object clone) + if (_addedProperties.Contains(alias) == false) + { + // add to the added props + _addedProperties.Add(alias); + } + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove) { - base.PerformDeepClone(clone); - - var clonedEntity = (Macro)clone; + // remove listening for changes + MacroProperty? prop = e.OldItems?.Cast().First(); + if (prop is not null) + { + prop.PropertyChanged -= PropertyDataChanged; - clonedEntity._addedProperties = new List(); - clonedEntity._removedProperties = new List(); - clonedEntity._properties = (MacroPropertyCollection)Properties.DeepClone(); - //re-assign the event handler - clonedEntity._properties.CollectionChanged += clonedEntity.PropertiesChanged; + 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 659334258e75..2a6f041fc0f2 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 { + private string _alias; + private string _editorAlias; + private int _id; + + private Guid _key; + private string? _name; + private int _sortOrder; + + public MacroProperty() + { + _editorAlias = string.Empty; + _alias = string.Empty; + _key = Guid.NewGuid(); + } + /// - /// Represents a Macro Property + /// Ctor for creating a new property /// - [Serializable] - [DataContract(IsReference = true)] - public class MacroProperty : BeingDirtyBase, IMacroProperty + /// + /// + /// + /// + public MacroProperty(string alias, string? name, int sortOrder, string editorAlias) { - public MacroProperty() - { - _editorAlias = string.Empty; - _alias = string.Empty; - _key = Guid.NewGuid(); - } + _alias = alias; + _name = name; + _sortOrder = sortOrder; + _key = Guid.NewGuid(); + _editorAlias = editorAlias; + } - /// - /// 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; + } - /// - /// 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)); + } - private Guid _key; - private string _alias; - private string? _name; - private int _sortOrder; - private int _id; - private string _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 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 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 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 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)); + } - /// - /// 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(); - public object DeepClone() + // 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)) { - //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; + return false; } - protected bool Equals(MacroProperty other) + if (ReferenceEquals(this, obj)) { - return string.Equals(_alias, other._alias) && _id == other._id; + return true; } - public override bool Equals(object? obj) + if (obj.GetType() != GetType()) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((MacroProperty) obj); + return false; } - public override int GetHashCode() + return Equals((MacroProperty)obj); + } + + protected bool Equals(MacroProperty 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/MacroPropertyCollection.cs b/src/Umbraco.Core/Models/MacroPropertyCollection.cs index cda46d2af7d9..c31cc8cbc113 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 { + public MacroPropertyCollection() + : base(property => property.Alias) + { + } + + public object DeepClone() + { + var clone = new MacroPropertyCollection(); + foreach (IMacroProperty item in this) + { + clone.Add((IMacroProperty)item.DeepClone()); + } + + return clone; + } + /// - /// A macro's property collection + /// Used to update an existing macro property /// - public class MacroPropertyCollection : ObservableDictionary, IDeepCloneable + /// + /// + /// + /// + /// The existing property alias + /// + /// + public void UpdateProperty(string currentAlias, string? name = null, int? sortOrder = null, string? editorAlias = null, string? newAlias = null) { - public MacroPropertyCollection() - : base(property => property.Alias) + IMacroProperty prop = this[currentAlias]; + if (prop == null) { + throw new InvalidOperationException("No property exists with alias " + currentAlias); } - public object DeepClone() + if (name.IsNullOrWhiteSpace() == false) { - var clone = new MacroPropertyCollection(); - foreach (var item in this) - { - clone.Add((IMacroProperty)item.DeepClone()); - } - return clone; + prop.Name = name; } - /// - /// 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) + if (sortOrder.HasValue) { - var prop = this[currentAlias]; - if (prop == null) - { - throw new InvalidOperationException("No property exists with alias " + currentAlias); - } + prop.SortOrder = sortOrder.Value; + } - 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 (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); - } + 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 072611da4c6f..02095596e779 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 +namespace Umbraco.Cms.Core.Models.Mapping; + +public class AuditMapDefinition : IMapDefinition { - public class AuditMapDefinition : IMapDefinition - { - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new AuditLog(), Map); - } + 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; - } + // 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 b185bb586e14..e9ba018f9ce0 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(Map); - mapper.Define(Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new CodeFileDisplay(), 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 3832654f451e..017ac1eb22fc 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.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) - { - _userService = userService; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; - _contentAppDefinitions = contentAppDefinitions; - _localizedTextService = localizedTextService; - } + public UserProfile? GetOwner(IContentBase source, MapperContext context) + { + IProfile? profile = source.GetCreatorProfile(_userService); + return profile == null ? null : context.Map(profile); + } - 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) + { + IProfile? profile = source.GetWriterProfile(_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) + { + IContentTypeComposition? contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source); + ContentTypeBasic? contentTypeBasic = context.Map(contentType); + return contentTypeBasic; + } - public ContentTypeBasic? GetContentType(IContentBase source, MapperContext context) - { - var contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source); - var contentTypeBasic = context.Map(contentType); - return contentTypeBasic; - } + public IEnumerable GetContentApps(IUmbracoEntity source) => GetContentAppsForEntity(source); - public IEnumerable GetContentApps(IUmbracoEntity source) - { - return GetContentAppsForEntity(source); - } + public IEnumerable GetContentAppsForEntity(IEntity source) + { + ContentApp[] apps = _contentAppDefinitions.GetContentAppsFor(source).ToArray(); - public IEnumerable GetContentAppsForEntity(IEntity source) + // localize content app names + foreach (ContentApp app in apps) { - 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 4becc8f21a2f..d3502cf887d5 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.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() { + private readonly IEntityService _entityService; + private readonly ILogger> _logger; + private readonly PropertyEditorCollection _propertyEditors; + + public ContentPropertyBasicMapper( + IDataTypeService dataTypeService, + IEntityService entityService, + ILogger> logger, + PropertyEditorCollection propertyEditors) + { + _logger = logger; + _propertyEditors = propertyEditors; + DataTypeService = dataTypeService; + _entityService = entityService; + } + + protected IDataTypeService DataTypeService { get; } + /// - /// Creates a base generic ContentPropertyBasic from a Property + /// Assigns the PropertyEditor, Id, Alias and Value to the property /// - internal class ContentPropertyBasicMapper - where TDestination : ContentPropertyBasic, new() + /// + public virtual void Map(IProperty property, TDestination dest, MapperContext context) { - private readonly IEntityService _entityService; - private readonly ILogger> _logger; - private readonly PropertyEditorCollection _propertyEditors; - protected IDataTypeService DataTypeService { get; } - - public ContentPropertyBasicMapper(IDataTypeService dataTypeService, IEntityService entityService, ILogger> logger, PropertyEditorCollection propertyEditors) + IDataEditor? editor = property.PropertyType is not null ? _propertyEditors[property.PropertyType.PropertyEditorAlias] : null; + if (editor == null) { - _logger = logger; - _propertyEditors = propertyEditors; - DataTypeService = dataTypeService; - _entityService = entityService; - } + _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]; - /// - /// Assigns the PropertyEditor, Id, Alias and Value to the property - /// - /// - public virtual void Map(IProperty property, TDestination dest, MapperContext context) - { - var editor = property.PropertyType is not null ? _propertyEditors[property.PropertyType.PropertyEditorAlias] : null; 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; + dest.Id = property.Id; + dest.Alias = property.Alias; + dest.PropertyEditor = editor; + dest.Editor = editor.Alias; + dest.DataTypeKey = property.PropertyType!.DataTypeKey; - //Get the culture from the context which will be set during the mapping operation for each property - var culture = context.GetCulture(); + // 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; + } - //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}"); + // Get the culture from the context which will be set during the mapping operation for each property + var culture = context.GetCulture(); - //set the culture to null if it's an invariant property type - culture = !property.PropertyType.VariesByCulture() ? null : culture; + // 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}"); + } - dest.Culture = culture; + // set the culture to null if it's an invariant property type + culture = !property.PropertyType.VariesByCulture() ? null : 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; + dest.Culture = culture; - // 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); + // 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 a31d9e9c2754..eb6c6d92e006 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.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; + 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) - { - _cultureDictionary = cultureDictionary; - _textService = textService; - } - public override void Map(IProperty originalProp, ContentPropertyDisplay dest, MapperContext context) - { - base.Map(originalProp, dest, context); + public ContentPropertyDisplayMapper( + ICultureDictionary cultureDictionary, + IDataTypeService dataTypeService, + IEntityService entityService, + ILocalizedTextService textService, + ILogger logger, + PropertyEditorCollection propertyEditors) + : base(dataTypeService, entityService, logger, propertyEditors) + { + _cultureDictionary = cultureDictionary; + _textService = textService; + } - var config = originalProp.PropertyType is null ? null : DataTypeService.GetDataType(originalProp.PropertyType.DataTypeId)?.Configuration; + public override void Map(IProperty originalProp, ContentPropertyDisplay dest, MapperContext context) + { + base.Map(originalProp, dest, context); - // 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? + var config = originalProp.PropertyType is null + ? null + : DataTypeService.GetDataType(originalProp.PropertyType.DataTypeId)?.Configuration; - // configure the editor for display with configuration - var valEditor = dest.PropertyEditor?.GetValueEditor(config); + // 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? - //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; + // configure the editor for display with configuration + IDataValueEditor? valEditor = dest.PropertyEditor?.GetValueEditor(config); - //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; + // 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; - 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; - } + // 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; - //Translate - dest.Label = _textService.UmbracoDictionaryTranslate(_cultureDictionary, dest.Label); - dest.Description = _textService.UmbracoDictionaryTranslate(_cultureDictionary, dest.Description); + 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); } } diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyDtoMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyDtoMapper.cs index fe1eff99caab..5836317b5c33 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 270d82138066..1e27389ebf8e 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.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) + { + _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) { - private readonly ContentPropertyBasicMapper _contentPropertyBasicConverter; - private readonly ContentPropertyDtoMapper _contentPropertyDtoConverter; - private readonly ContentPropertyDisplayMapper _contentPropertyDisplayMapper; + 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); + } - 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); - } + // 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; + } - 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); - } + private void Map(IProperty source, ContentPropertyBasic target, MapperContext context) => - // 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; - } + // assume this is mapping everything and no MapAll is required + _contentPropertyBasicConverter.Map(source, target, context); - 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) => - private void Map(IProperty source, ContentPropertyDto target, MapperContext context) - { - // assume this is mapping everything and no MapAll is required - _contentPropertyDtoConverter.Map(source, target, 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); - } - } + 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 a087ce0d3e67..ba06dae711e9 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 ContentSavedStateMapper(); + private readonly ContentSavedStateMapper _inner = new(); - public ContentSavedState? Map(IContent source, MapperContext context) - { - return _inner.Map(source, context); - } - } + public ContentSavedState? Map(IContent source, MapperContext context) => _inner.Map(source, context); +} - /// - /// Returns the for an item - /// - /// - public class ContentSavedStateMapper - where T : ContentPropertyBasic +/// +/// Returns the for an item +/// +/// +public class ContentSavedStateMapper + where T : ContentPropertyBasic +{ + public ContentSavedState Map(IContent source, MapperContext context) { - public ContentSavedState Map(IContent source, MapperContext context) + PublishedState publishedState; + bool isEdited; + bool isCreated; + + if (source.ContentType.VariesByCulture()) { - PublishedState publishedState; - bool isEdited; - bool isCreated; + // Get the culture from the context which will be set during the mapping operation for each variant + var culture = context.GetCulture(); - 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(); - - //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"); + 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; - } + isEdited = source.IsCultureEdited(culture); + isCreated = source.Id > 0 && source.IsCultureAvailable(culture); + } + else + { + publishedState = source.PublishedState == PublishedState.Unpublished + ? PublishedState.Unpublished + : PublishedState.Published; - if (!isCreated) - return ContentSavedState.NotCreated; + isEdited = source.Edited; + isCreated = source.Id > 0; + } - if (publishedState == PublishedState.Unpublished) - return ContentSavedState.Draft; + if (!isCreated) + { + return ContentSavedState.NotCreated; + } - if (publishedState == PublishedState.Published) - return isEdited ? ContentSavedState.PublishedPendingChanges : ContentSavedState.Published; + if (publishedState == PublishedState.Unpublished) + { + return ContentSavedState.Draft; + } - throw new NotSupportedException($"PublishedState {publishedState} is not supported."); + 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 bacab0b7cf7b..31c2e86b5b67 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.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; - - - [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>()) - { - } - - 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; - - _contentSettings = contentSettings.CurrentValue; - contentSettings.OnChange(x => _contentSettings = x); - } - - 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) => new DocumentTypeDisplay(), Map); - mapper.Define((source, context) => new MediaTypeDisplay(), Map); - mapper.Define((source, context) => new MemberTypeDisplay(), 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); - } + } - return new PropertyType(_shortStringHelper, dataType, source.Alias); - }, Map); + 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; + + _contentSettings = contentSettings.CurrentValue; + contentSettings.OnChange(x => _contentSettings = x); + } - // TODO: isPublishing in ctor? - mapper.Define, PropertyGroup>( - (source, context) => new PropertyGroup(false), Map); - mapper.Define, PropertyGroup>( - (source, context) => new PropertyGroup(false), Map); + public static Udi? MapContentTypeUdi(IContentTypeComposition source) + { + if (source == null) + { + return null; + } - 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); + 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"); + } - mapper.Define((source, context) => new DocumentTypeDisplay(), Map); - mapper.Define((source, context) => new MediaTypeDisplay(), Map); - mapper.Define((source, context) => new MemberTypeDisplay(), Map); + return Udi.Create(udiType, source.Key); + } - mapper.Define, PropertyGroupDisplay>( - (source, context) => new PropertyGroupDisplay(), Map); - mapper.Define, PropertyGroupDisplay>( - (source, context) => new PropertyGroupDisplay(), 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) => new DocumentTypeDisplay(), Map); + mapper.Define((source, context) => new MediaTypeDisplay(), Map); + mapper.Define((source, context) => new MemberTypeDisplay(), 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 PropertyTypeDisplay(), Map); - mapper.Define( - (source, context) => new MemberPropertyTypeDisplay(), 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) + 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) { - 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.HistoryCleanup = null; + return; + } - target.SetDefaultTemplate(source.DefaultTemplate == null - ? null - : _fileService.GetTemplate(source.DefaultTemplate)); + // 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; } - private static void MapHistoryCleanup(DocumentTypeSave source, IContentTypeWithHistoryCleanup target) + if (target.HistoryCleanup.KeepAllVersionsNewerThanDays != source.HistoryCleanup.KeepAllVersionsNewerThanDays) { - // If source history cleanup is null we don't have to map all properties - if (source.HistoryCleanup is null) - { - target.HistoryCleanup = null; - return; - } + target.HistoryCleanup.KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays; + } - // 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.KeepLatestVersionPerDayForDays != + source.HistoryCleanup.KeepLatestVersionPerDayForDays) + { + target.HistoryCleanup.KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays; + } + } - if (target.HistoryCleanup.KeepAllVersionsNewerThanDays != source.HistoryCleanup.KeepAllVersionsNewerThanDays) - { - target.HistoryCleanup.KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays; - } + // 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 (target.HistoryCleanup.KeepLatestVersionPerDayForDays != - source.HistoryCleanup.KeepLatestVersionPerDayForDays) + if (source.GroupId > 0) + { + if (target.PropertyGroupId?.Value != source.GroupId) { - target.HistoryCleanup.KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays; + target.PropertyGroupId = new Lazy(() => source.GroupId, false); } } - // no MapAll - take care - private void Map(MediaTypeSave source, IMediaType target, MapperContext context) + 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) { - MapSaveToTypeBase(source, target, context); - MapComposition(source, target, alias => _mediaTypeService.Get(alias)); + target.Id = source.Id; } - // no MapAll - take care - private void Map(MemberTypeSave source, IMemberType target, MapperContext context) - { - MapSaveToTypeBase(source, target, context); - MapComposition(source, target, alias => _memberTypeService.Get(alias)); + target.Key = source.Key; + target.Type = source.Type; + target.Name = source.Name; + target.Alias = source.Alias; + target.SortOrder = source.SortOrder; + } - 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; - } + // no MapAll - take care + private void Map(DocumentTypeSave source, IContentType target, MapperContext context) + { + MapSaveToTypeBase(source, target, context); + MapComposition(source, target, alias => _contentTypeService.Get(alias)); - target.SetMemberCanEditProperty(localCopy.Alias, localCopy.MemberCanEditProperty); - target.SetMemberCanViewProperty(localCopy.Alias, localCopy.MemberCanViewProperty); - target.SetIsSensitiveProperty(localCopy.Alias, localCopy.IsSensitiveData); - } + if (target is IContentTypeWithHistoryCleanup targetWithHistoryCleanup) + { + MapHistoryCleanup(source, targetWithHistoryCleanup); } - // no MapAll - take care - private void Map(IContentType source, DocumentTypeDisplay target, MapperContext context) - { - MapTypeToDisplayBase(source, target); + target.AllowedTemplates = source.AllowedTemplates? + .Where(x => x != null) + .Select(_fileService.GetTemplate) + .WhereNotNull() + .ToArray(); - 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.SetDefaultTemplate(source.DefaultTemplate == null + ? null + : _fileService.GetTemplate(source.DefaultTemplate)); + } - target.AllowCultureVariant = source.VariesByCulture(); - target.AllowSegmentVariant = source.VariesBySegment(); - target.ContentApps = _commonMapper.GetContentAppsForEntity(source); + // no MapAll - take care + private void Map(MediaTypeSave source, IMediaType target, MapperContext context) + { + MapSaveToTypeBase(source, target, context); + MapComposition(source, target, alias => _mediaTypeService.Get(alias)); + } - //sync templates - if (source.AllowedTemplates is not null) - { - target.AllowedTemplates = context.MapEnumerable(source.AllowedTemplates).WhereNotNull(); - } + // no MapAll - take care + private void Map(MemberTypeSave source, IMemberType target, MapperContext context) + { + MapSaveToTypeBase(source, target, context); + MapComposition(source, target, alias => _memberTypeService.Get(alias)); - if (source.DefaultTemplate != null) + 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) { - target.DefaultTemplate = context.Map(source.DefaultTemplate); + continue; } - //default listview - target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Content"; + target.SetMemberCanEditProperty(localCopy.Alias, localCopy.MemberCanEditProperty); + target.SetMemberCanViewProperty(localCopy.Alias, localCopy.MemberCanViewProperty); + target.SetIsSensitiveProperty(localCopy.Alias, localCopy.IsSensitiveData); + } + } - if (string.IsNullOrEmpty(source.Alias)) - { - return; - } + // no MapAll - take care + private void Map(IContentType source, DocumentTypeDisplay target, MapperContext context) + { + MapTypeToDisplayBase(source, target); - var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Alias; - if (_dataTypeService.GetDataType(name) != null) - { - target.ListViewEditorName = name; - } + 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, + }; } - // no MapAll - take care - private void Map(IMediaType source, MediaTypeDisplay target, MapperContext context) + target.AllowCultureVariant = source.VariesByCulture(); + target.AllowSegmentVariant = source.VariesBySegment(); + target.ContentApps = _commonMapper.GetContentAppsForEntity(source); + + // sync templates + if (source.AllowedTemplates is not null) { - MapTypeToDisplayBase(source, target); + target.AllowedTemplates = + context.MapEnumerable(source.AllowedTemplates).WhereNotNull(); + } - //default listview - target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Media"; - target.IsSystemMediaType = source.IsSystemMediaType(); + if (source.DefaultTemplate != null) + { + target.DefaultTemplate = context.Map(source.DefaultTemplate); + } - if (string.IsNullOrEmpty(source.Name)) - { - return; - } + // default listview + target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Content"; - var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Name; - if (_dataTypeService.GetDataType(name) != null) - { - target.ListViewEditorName = name; - } + if (string.IsNullOrEmpty(source.Alias)) + { + return; } - // no MapAll - take care - private void Map(IMemberType source, MemberTypeDisplay target, MapperContext context) + var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Alias; + if (_dataTypeService.GetDataType(name) != null) { - MapTypeToDisplayBase(source, target); + target.ListViewEditorName = name; + } + } - //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; - } + // no MapAll - take care + private void Map(IMediaType source, MediaTypeDisplay target, MapperContext context) + { + MapTypeToDisplayBase(source, target); - displayProp.MemberCanEditProperty = source.MemberCanEditProperty(localCopy.Alias); - displayProp.MemberCanViewProperty = source.MemberCanViewProperty(localCopy.Alias); - displayProp.IsSensitiveData = source.IsSensitiveProperty(localCopy.Alias); - } - } + // default listview + target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Media"; + target.IsSystemMediaType = source.IsSystemMediaType(); - // Umbraco.Code.MapAll -Blueprints - private void Map(IContentTypeBase source, ContentTypeBasic target, string entityType) + if (string.IsNullOrEmpty(source.Name)) { - 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}"; + return; + } - 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); + var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Name; + if (_dataTypeService.GetDataType(name) != null) + { + target.ListViewEditorName = name; + } + } - if (source.Id > 0) - { - target.Id = source.Id; - } + // no MapAll - take care + private void Map(IMemberType source, MemberTypeDisplay target, MapperContext context) + { + MapTypeToDisplayBase(source, target); - if (source.GroupId > 0) + // 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) { - if (target.PropertyGroupId?.Value != source.GroupId) - { - target.PropertyGroupId = new Lazy(() => source.GroupId, false); - } + continue; } - target.Alias = source.Alias; - target.Description = source.Description; - target.SortOrder = source.SortOrder; - target.LabelOnTop = source.LabelOnTop; + displayProp.MemberCanEditProperty = source.MemberCanEditProperty(localCopy.Alias); + displayProp.MemberCanViewProperty = source.MemberCanViewProperty(localCopy.Alias); + displayProp.IsSensitiveData = source.IsSensitiveProperty(localCopy.Alias); } + } - // no MapAll - take care - private void Map(DocumentTypeSave source, DocumentTypeDisplay target, MapperContext context) - { - MapTypeToDisplayBase(source, - target, context); + // 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; + } - //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(); - } + // 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); - 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 - 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 - take care - private void Map(MediaTypeSave source, MediaTypeDisplay target, MapperContext context) => - MapTypeToDisplayBase(source, - target, context); + // 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 - take care - private void Map(MemberTypeSave source, MemberTypeDisplay target, MapperContext context) => - MapTypeToDisplayBase( - source, target, context); + // 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 -UpdateDate -DeleteDate -Key -PropertyTypes - private static void Map(PropertyGroupBasic source, PropertyGroup target, - MapperContext context) - { - if (source.Id > 0) - { - target.Id = source.Id; - } + // no MapAll - take care + private void Map(DocumentTypeSave source, DocumentTypeDisplay target, MapperContext context) + { + MapTypeToDisplayBase( + source, + target, + context); - target.Key = source.Key; - target.Type = source.Type; - target.Name = source.Name; - target.Alias = source.Alias; - target.SortOrder = source.SortOrder; - } + // sync templates + IEnumerable destAllowedTemplateAliases = target.AllowedTemplates.Select(x => x.Alias); - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate -Key -PropertyTypes - private static void Map(PropertyGroupBasic source, PropertyGroup target, - MapperContext context) + // 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) { - 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; + 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(); } - // Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames - private static void Map(PropertyGroupBasic source, - PropertyGroupDisplay target, MapperContext context) + if (source.DefaultTemplate.IsNullOrWhiteSpace() == false) { - target.Inherited = source.Inherited; - if (source.Id > 0) + // 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) { - target.Id = source.Id; + ITemplate? template = _fileService.GetTemplate(source.DefaultTemplate); + target.DefaultTemplate = template == null ? null : context.Map(template); } - - 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) + else { - target.Inherited = source.Inherited; - if (source.Id > 0) - { - target.Id = source.Id; - } + target.DefaultTemplate = null; + } + } - 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; - } + // no MapAll - take care + private void Map(MediaTypeSave source, MediaTypeDisplay target, MapperContext context) => + MapTypeToDisplayBase( + source, + target, + context); - 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; + // no MapAll - take care + private void Map(MemberTypeSave source, MemberTypeDisplay target, MapperContext context) => + MapTypeToDisplayBase( + source, target, context); - target.AllowedAsRoot = source.AllowAsRoot; + // 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; + } - bool allowedContentTypesUnchanged = target.AllowedContentTypes?.Select(x => x.Id.Value) - .SequenceEqual(source.AllowedContentTypes) ?? false; + target.Key = source.Key; + target.Type = source.Type; + target.Name = source.Name; + target.Alias = source.Alias; + target.SortOrder = source.SortOrder; + } - if (allowedContentTypesUnchanged is false) - { - target.AllowedContentTypes = source.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i)); - } + // 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(); + } - if (!(target is IMemberType)) - { - target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); - target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); - } + // 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; + } - // 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; - } + 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(); + } - // ensure no duplicate alias, then assign the group properties collection - EnsureUniqueAliases(destProperties); + // 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; + } - if (destGroup is not null) - { - if (destGroup.PropertyTypes?.SupportsPublishing != isPublishing || destGroup.PropertyTypes.SequenceEqual(destProperties) is false) - { - destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties); - } + // 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; + } - destGroups.Add(destGroup); - } - } + // 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; - // ensure no duplicate name, then assign the groups collection - EnsureUniqueAliases(destGroups); + var id = Convert.ToInt32(source.Id); + if (id > 0) + { + target.Id = id; + } - if (target.PropertyGroups.SequenceEqual(destGroups) is false) - { - target.PropertyGroups = new PropertyGroupCollection(destGroups); - } + 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; - // 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 + target.AllowedAsRoot = source.AllowAsRoot; - // 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); - } + var allowedContentTypesUnchanged = target.AllowedContentTypes?.Select(x => x.Id.Value) + .SequenceEqual(source.AllowedContentTypes) ?? false; - // 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 + if (allowedContentTypesUnchanged is false) + { + target.AllowedContentTypes = source.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i)); } - // Umbraco.Code.MapAll -Blueprints -Errors -ListViewEditorName -Trashed - private void MapTypeToDisplayBase(IContentTypeComposition source, ContentTypeCompositionDisplay target) + if (!(target is IMemberType)) { - 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(); - } + target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); + target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); + } - IContentType? parent = _contentTypeService.Get(source.ParentId); - if (parent == null) + // 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)) { - return Enumerable.Empty(); + continue; } - 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) + // ensure no duplicate alias, then assign the group properties collection + EnsureUniqueAliases(destProperties); + + if (destGroup is not null) { - foreach (var ancestorId in ancestorIds) + if (destGroup.PropertyTypes?.SupportsPublishing != isPublishing || + destGroup.PropertyTypes.SequenceEqual(destProperties) is false) { - IContentType? ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); - if (ancestor is not null && ancestor.Alias is not null) - { - aliases.Add(ancestor.Alias); - } + destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties); } + + destGroups.Add(destGroup); } + } + // ensure no duplicate name, then assign the groups collection + EnsureUniqueAliases(destGroups); - return aliases.OrderBy(x => x); + if (target.PropertyGroups.SequenceEqual(destGroups) is false) + { + target.PropertyGroups = new PropertyGroupCollection(destGroups); } - public static Udi? MapContentTypeUdi(IContentTypeComposition source) + // 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) { - if (source == null) - { - return null; - } + // handle local properties + IPropertyType[] destProperties = genericPropertiesGroup.Properties + .Where(x => x.Inherited == false) + .Select(x => MapSaveProperty(x, destOrigProperties, context)) + .WhereNotNull() + .ToArray(); - 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"); - } + // 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 + } - return Udi.Create(udiType, source.Key); + // 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(); } - private static PropertyGroup? MapSaveGroup(PropertyGroupBasic sourceGroup, - IEnumerable destOrigGroups, MapperContext context) - where TPropertyType : PropertyTypeBasic + 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) { - PropertyGroup? destGroup; - if (sourceGroup.Id > 0) + foreach (var ancestorId in ancestorIds) { - // update an existing group - // ensure it is still there, then map/update - destGroup = destOrigGroups.FirstOrDefault(x => x.Id == sourceGroup.Id); - if (destGroup != null) + IContentType? ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); + if (ancestor is not null && ancestor.Alias is not null) { - context.Map(sourceGroup, destGroup); - return destGroup; + aliases.Add(ancestor.Alias); } - - // 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) + return aliases.OrderBy(x => x); + } + + private static PropertyGroup? MapSaveGroup( + PropertyGroupBasic sourceGroup, + IEnumerable destOrigGroups, + MapperContext context) + where TPropertyType : PropertyTypeBasic + { + PropertyGroup? destGroup; + if (sourceGroup.Id > 0) { - IPropertyType? destProperty; - if (sourceProperty.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) { - // 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; + context.Map(sourceGroup, destGroup); + return destGroup; } - // 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; + // force-clear the ID as it does not match anything + sourceGroup.Id = 0; } - private static void EnsureUniqueAliases(IEnumerable properties) + // 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) { - IPropertyType[] propertiesA = properties.ToArray(); - var distinctProperties = propertiesA - .Select(x => x.Alias?.ToUpperInvariant()) - .Distinct() - .Count(); - if (distinctProperties != propertiesA.Length) + // updating an existing property + // ensure it is still there, then map/update + destProperty = destOrigProperties.FirstOrDefault(x => x.Id == sourceProperty.Id); + if (destProperty != null) { - throw new InvalidOperationException("Cannot map properties due to alias conflict."); + context.Map(sourceProperty, destProperty); + return destProperty; } + + // force-clear the ID as it does not match anything + sourceProperty.Id = 0; } - private static void EnsureUniqueAliases(IEnumerable groups) + // 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) { - 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."); - } + throw new InvalidOperationException("Cannot map properties due to alias conflict."); } + } - private static void MapComposition(ContentTypeSave source, IContentTypeComposition target, - Func getContentType) + private static void EnsureUniqueAliases(IEnumerable groups) + { + PropertyGroup[] groupsA = groups.ToArray(); + var distinctProperties = groupsA + .Select(x => x.Alias) + .Distinct() + .Count(); + if (distinctProperties != groupsA.Length) { - var current = target.CompositionAliases().ToArray(); - IEnumerable proposed = source.CompositeContentTypes; + throw new InvalidOperationException("Cannot map groups due to alias conflict."); + } + } - IEnumerable remove = current.Where(x => !proposed.Contains(x)); - IEnumerable add = proposed.Where(x => !current.Contains(x)); + private static void MapComposition(ContentTypeSave source, IContentTypeComposition target, Func getContentType) + { + var current = target.CompositionAliases().ToArray(); + IEnumerable proposed = source.CompositeContentTypes; - foreach (var alias in remove) - { - target.RemoveContentType(alias); - } + 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) + foreach (var alias in add) + { + // TODO: Remove N+1 lookup + IContentTypeComposition? contentType = getContentType(alias); + if (contentType != null) { - // 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 2f330b581fc6..7ad8b987d777 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; + private readonly ILocalizationService _localizationService; + private readonly ILocalizedTextService _localizedTextService; - public ContentVariantMapper(ILocalizationService localizationService, ILocalizedTextService localizedTextService) - { - _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); - _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); - } + public ContentVariantMapper(ILocalizationService localizationService, ILocalizedTextService localizedTextService) + { + _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); + _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); + } - public IEnumerable Map(IContent source, MapperContext context) where TVariant : ContentVariantDisplay - { - var variesByCulture = source.ContentType.VariesByCulture(); - var variesBySegment = source.ContentType.VariesBySegment(); + public IEnumerable Map(IContent source, MapperContext context) + where TVariant : ContentVariantDisplay + { + var variesByCulture = source.ContentType.VariesByCulture(); + var variesBySegment = source.ContentType.VariesBySegment(); - List variants = new (); + 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 + if (!variesByCulture && !variesBySegment) + { + // this is invariant so just map the IContent instance to ContentVariationDisplay + TVariant? variantDisplay = context.Map(source); + if (variantDisplay is not null) { - // 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(); + variants.Add(variantDisplay); } - - return SortVariants(variants); } - - private IList SortVariants(IList variants) where TVariant : ContentVariantDisplay + else if (variesByCulture && !variesBySegment) + { + IEnumerable languages = GetLanguages(context); + variants = languages + .Select(language => CreateVariantDisplay(context, source, language, null)) + .WhereNotNull() + .ToList(); + } + else if (variesBySegment && !variesByCulture) + { + // Segment only + IEnumerable segments = GetSegments(source); + variants = segments + .Select(segment => CreateVariantDisplay(context, source, null, segment)) + .WhereNotNull() + .ToList(); + } + else { - if (variants.Count <= 1) + // 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 variant.Segment == null; - } + return SortVariants(variants); + } + + private static bool IsDefaultSegment(ContentVariantDisplay variant) => variant.Segment == null; - private static bool IsDefaultLanguage(ContentVariantDisplay variant) + private IList SortVariants(IList variants) + where TVariant : ContentVariantDisplay + { + if ( variants.Count <= 1) { - return variant.Language == null || variant.Language.IsDefault; + return variants; } - private IEnumerable GetLanguages(MapperContext context) + // 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) { - 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(); - } + // This should never happen + return Enumerable.Empty(); } - /// - /// 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 }; + return context.MapEnumerable(allLanguages).WhereNotNull().ToList(); + } - // Add actual segments based on the property values - segments.AddRange(content.Properties.SelectMany(p => p.Values.Select(v => v.Segment))); + /// + /// 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 }; - // Do not return a segment more than once - return segments.Distinct(); - } + // Add actual segments based on the property values + segments.AddRange(content.Properties.SelectMany(p => p.Values.Select(v => v.Segment))); - private TVariant? CreateVariantDisplay(MapperContext context, IContent content, ContentEditing.Language? language, string? segment) where TVariant : ContentVariantDisplay - { - context.SetCulture(language?.IsoCode); - context.SetSegment(segment); + // Do not return a segment more than once + return segments.Distinct(); + } - var variantDisplay = context.Map(content); + private TVariant? CreateVariantDisplay(MapperContext context, IContent content, ContentEditing.Language? language, string? segment) + where TVariant : ContentVariantDisplay + { + context.SetCulture(language?.IsoCode); + context.SetSegment(segment); - if (variantDisplay is null) - { - return null; - } - variantDisplay.Segment = segment; - variantDisplay.Language = language; - variantDisplay.Name = content.GetCultureName(language?.IsoCode); - variantDisplay.DisplayName = GetDisplayName(language, segment); + TVariant? variantDisplay = context.Map(content); - return variantDisplay; + if (variantDisplay is null) + { + return null; } - private string GetDisplayName(ContentEditing.Language? language, string? segment) - { - var isCultureVariant = language is not null; - var isSegmentVariant = !segment.IsNullOrWhiteSpace(); + variantDisplay.Segment = segment; + variantDisplay.Language = language; + variantDisplay.Name = content.GetCultureName(language?.IsoCode); + variantDisplay.DisplayName = GetDisplayName(language, segment); - if(!isCultureVariant && !isSegmentVariant) - { - return _localizedTextService.Localize("general", "default"); - } + return variantDisplay; + } - var parts = new List(); + private string GetDisplayName(ContentEditing.Language? language, string? segment) + { + var isCultureVariant = language is not null; + var isSegmentVariant = !segment.IsNullOrWhiteSpace(); - if (isSegmentVariant) - parts.Add(segment!); + if (!isCultureVariant && !isSegmentVariant) + { + return _localizedTextService.Localize("general", "default"); + } - if (isCultureVariant) - parts.Add(language?.Name!); + var parts = new List(); - return string.Join(" — ", parts); + if (isSegmentVariant) + { + parts.Add(segment!); + } + if (isCultureVariant) + { + 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 f1fc81cd2420..de2a773257b5 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.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) - { - _propertyEditors = propertyEditors; - _logger = logger; - _contentSettings = contentSettings.Value ?? throw new ArgumentNullException(nameof(contentSettings)); - _serializer = serializer; - } + private readonly ContentSettings _contentSettings; + private readonly ILogger _logger; + private readonly PropertyEditorCollection _propertyEditors; + private readonly IConfigurationEditorJsonSerializer _serializer; - private static readonly int[] SystemIds = - { - Constants.DataTypes.DefaultContentListView, - Constants.DataTypes.DefaultMediaListView, - Constants.DataTypes.DefaultMembersListView - }; + 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); - } + 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 + 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 -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 -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) + // 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)) { - 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; + return; } - // 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; - } + 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) + // 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)) { - target.DatabaseType = MapDatabaseType(source); - target.Editor = _propertyEditors[source.EditorAlias]; - target.Id = Convert.ToInt32(source.Id); - target.Name = source.Name; - target.ParentId = source.ParentId; + return; } - private IEnumerable MapAvailableEditors(IDataType source, 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)) { - var properties = _propertyEditors - .Where(x => !x.IsDeprecated || _contentSettings.ShowDeprecatedPropertyEditors || source.EditorAlias == x.Alias) - .OrderBy(x => x.Name); - return context.MapEnumerable(properties).WhereNotNull(); + throw new InvalidOperationException( + $"Could not find a property editor with alias \"{dataType.EditorAlias}\"."); } - 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 + IConfigurationEditor configurationEditor = editor.GetConfigurationEditor(); + var fields = context + .MapEnumerable(configurationEditor.Fields) + .WhereNotNull().ToList(); + IDictionary configurationDictionary = + configurationEditor.ToConfigurationEditor(dataType.Configuration); - 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}\"."); + MapConfigurationFields(dataType, fields, configurationDictionary); - var configurationEditor = editor!.GetConfigurationEditor(); - var fields = context.MapEnumerable(configurationEditor.Fields).WhereNotNull().ToList(); - var configurationDictionary = configurationEditor.ToConfigurationEditor(dataType.Configuration); + return fields; + } - MapConfigurationFields(dataType, fields, configurationDictionary); + private void MapConfigurationFields(IDataType? dataType, List fields, IDictionary? configuration) + { + if (fields == null) + { + throw new ArgumentNullException(nameof(fields)); + } - return fields; + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); } - private void MapConfigurationFields(IDataType? dataType, List fields, IDictionary? configuration) + // now we need to wire up the pre-values values with the actual fields defined + foreach (DataTypeConfigurationFieldDisplay field in fields.ToList()) { - if (fields == null) throw new ArgumentNullException(nameof(fields)); - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + // 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; + } - // now we need to wire up the pre-values values with the actual fields defined - foreach (var field in fields.ToList()) + if (field.Key is not null && configuration.TryGetValue(field.Key, out var value)) + { + field.Value = value; + } + else { - //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; - } - - 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); - } + // 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) - { - // 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); + // 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); + } - return fields; + 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) + { + 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 a5db1d4b9621..cab595e00fa1 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) + { + _localizationService = localizationService; + _commonMapper = commonMapper; + } + + 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) { - private readonly ILocalizationService _localizationService; - private readonly CommonMapper? _commonMapper; + target.Alias = source.ItemKey; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.ItemKey; + } - [Obsolete("Use the constructor with the CommonMapper")] - public DictionaryMapDefinition(ILocalizationService localizationService) + 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); } + } - public void DefineMaps(IUmbracoMapper mapper) + // 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) { - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new DictionaryDisplay(), Map); - mapper.Define((source, context) => new DictionaryOverviewDisplay(), Map); + target.ContentApps.AddRange(_commonMapper.GetContentAppsForEntity(source)); } - // Umbraco.Code.MapAll -ParentId -Path -Trashed -Udi -Icon - private static void Map(IDictionaryItem source, EntityBasic target, MapperContext context) + // 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) { - target.Alias = source.ItemKey; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.ItemKey; + 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); } - - // Umbraco.Code.MapAll -Icon -Trashed -Alias - private void Map(IDictionaryItem source, DictionaryDisplay target, MapperContext context) + else { - 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)); - } - - // 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 - { - IsoCode = lang.IsoCode, - DisplayName = lang.CultureName, - Translation = translation?.Value ?? string.Empty, - LanguageId = lang.Id - }); - } + target.Path = "-1," + source.Id; } - // Umbraco.Code.MapAll -Level -Translations - private void Map(IDictionaryItem source, DictionaryOverviewDisplay target, MapperContext context) + // add all languages and the translations + foreach (ILanguage lang in _localizationService.GetAllLanguages()) { - target.Id = source.Id; - target.Name = source.ItemKey; + var langId = lang.Id; + IDictionaryTranslation? translation = source.Translations?.FirstOrDefault(x => x.LanguageId == langId); - // add all languages and the translations - foreach (var lang in _localizationService.GetAllLanguages()) + target.Translations.Add(new DictionaryTranslationDisplay { - 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 - }); - } + IsoCode = lang.IsoCode, + DisplayName = lang.CultureName, + Translation = translation?.Value ?? string.Empty, + LanguageId = lang.Id, + }); } + } - private static void GetParentId(Guid parentId, ILocalizationService localizationService, List ids) - { - var dictionary = localizationService.GetDictionaryItemById(parentId); - if (dictionary == null) - return; + // Umbraco.Code.MapAll -Level -Translations + private void Map(IDictionaryItem source, DictionaryOverviewDisplay target, MapperContext context) + { + target.Id = source.Id; + target.Name = source.ItemKey; - ids.Add(dictionary.Id); + // add all languages and the translations + foreach (ILanguage lang in _localizationService.GetAllLanguages()) + { + var langId = lang.Id; + IDictionaryTranslation? translation = source.Translations?.FirstOrDefault(x => x.LanguageId == langId); - if (dictionary.ParentId.HasValue) - GetParentId(dictionary.ParentId.Value, localizationService, ids); + target.Translations.Add( + new DictionaryOverviewTranslationDisplay + { + DisplayName = lang.CultureName, + HasTranslation = translation != null && string.IsNullOrEmpty(translation.Value) == false, + }); } } } diff --git a/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs index 7450ec62b43c..81096889c8f6 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); - } + 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 -Udi -Path -Trashed -AdditionalData -Icon - private static void Map(ILanguage source, EntityBasic target, MapperContext context) + // 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) { - target.Name = source.CultureName; - target.Key = source.Key; - target.ParentId = -1; - target.Alias = source.IsoCode; - target.Id = source.Id; + throw new ArgumentNullException(nameof(target)); } - // Umbraco.Code.MapAll - private static void Map(ILanguage source, ContentEditing.Language target, MapperContext context) + if (!(target is List list)) { - target.Id = source.Id; - target.IsoCode = source.IsoCode; - target.Name = source.CultureName; - target.IsDefault = source.IsDefault; - target.IsMandatory = source.IsMandatory; - target.FallbackLanguageId = source.FallbackLanguageId; + throw new NotSupportedException($"{nameof(target)} must be a List."); } - private static void Map(IEnumerable source, IEnumerable 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) { - 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 13fe7f7c335c..a042497013e9 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) - { - _parameterEditors = parameterEditors; - _logger = 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); + } - 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); + } - // 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; - private void Map(IMacro source, MacroDisplay target, MapperContext context) + // 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) { - 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; + // 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); } - // 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; + 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 89cdc2210685..70d4826ab612 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 - { - private const string CultureKey = "Map.Culture"; - private const string SegmentKey = "Map.Segment"; - private const string IncludedPropertiesKey = "Map.IncludedProperties"; + 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 culture. - /// - public static string? GetCulture(this MapperContext context) - { - 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; - } + /// + /// 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) + /// + /// Sets a context culture. + /// + public static void SetCulture(this MapperContext context, string? culture) + { + if (culture is not null) { - if (culture is not null) - { - context.Items[CultureKey] = culture; - } + context.Items[CultureKey] = culture; } + } - /// - /// Sets a context segment. - /// - public static void SetSegment(this MapperContext context, string? segment) + /// + /// Sets a context segment. + /// + public static void SetSegment(this MapperContext context, string? segment) + { + if (segment is not null) { - if (segment is not null) - { - context.Items[SegmentKey] = segment; - } + 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; - } + /// + /// 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; - } - } + /// + /// 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 c2c3e14f5da9..8444d5bd0a63 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); + public void DefineMaps(IUmbracoMapper mapper) => mapper.Define(Map); - 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; + 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 9a3905159089..ae9876628fea 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.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) - { - _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; - } + /// + /// Overridden to deal with custom member properties and permissions. + public override IEnumerable> Map(IMember source, MapperContext context) + { - /// - /// Overridden to deal with custom member properties and permissions. - public override IEnumerable> Map(IMember source, MapperContext context) - { + IMemberType? memberType = _memberTypeService.Get(source.ContentTypeId); - var memberType = _memberTypeService.Get(source.ContentTypeId); + if (memberType is not null) + { - if (memberType is not null) - { + IgnoreProperties = memberType.CompositionPropertyTypes + .Where(x => x.HasIdentity == false) + .Select(x => x.Alias) + .ToArray(); + } - IgnoreProperties = memberType.CompositionPropertyTypes - .Where(x => x.HasIdentity == false) - .Select(x => x.Alias) - .ToArray(); - } + IEnumerable> resolved = base.Map(source, context); - var resolved = base.Map(source, context); + return resolved; + } - 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); + } - [Obsolete("Use MapMembershipProperties. Will be removed in Umbraco 10.")] - protected override IEnumerable GetCustomGenericProperties(IContentBase content) + private Dictionary GetPasswordConfig(IMember member) + { + var result = new Dictionary(_memberPasswordConfiguration.GetConfiguration(true)) { - var member = (IMember)content; - return MapMembershipProperties(member, null); - } + // the password change toggle will only be displayed if there is already a password assigned. + {"hasPassword", member.RawPasswordValue.IsNullOrWhiteSpace() == false} + }; - 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} - }; + // 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; - // 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; + } - 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); - /// - /// Overridden to assign the IsSensitive property values - /// - /// - /// - /// - /// - protected override List MapProperties(IContentBase content, List properties, MapperContext context) + // now update the IsSensitive value + foreach (ContentPropertyDisplay prop in result) { - var result = base.MapProperties(content, properties, context); - var member = (IMember)content; - var memberType = _memberTypeService.Get(member.ContentTypeId); - - // now update the IsSensitive value - foreach (var 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) { - // 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)) - { - // 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; - } + // 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; } - 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; } + return result; + } - internal IDictionary GetMemberGroupValue(string username) + /// + /// 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 { - IEnumerable userRoles = _memberService.GetAllRoles(username); + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login", + Label = localizedText.Localize(null,"login"), + Value = member.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); + prop.View = "textbox"; + prop.Validation.Mandatory = true; + return prop; + } - // if user has no roles, just return the dictionary - if (userRoles == null) - { - return result; - } + internal IDictionary GetMemberGroupValue(string username) + { + IEnumerable userRoles = _memberService.GetAllRoles(username); - // 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; - } + // 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; } - public IEnumerable MapMembershipProperties(IMember member, 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 properties = new List + result[userRole] = true; + } + + return result; + } + + public IEnumerable MapMembershipProperties(IMember member, MapperContext? context) + { + var properties = new List + { + GetLoginProperty(member, _localizedTextService), + new() { - 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}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 { - 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 + // TODO: why ignoreCase, what are we doing here?! + { "newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null) } }, - new() + 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 { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}membergroup", - Label = _localizedTextService.Localize("content","membergroup"), - Value = GetMemberGroupValue(member.Username), - View = "membergroups", - Config = new Dictionary - { - { "IsRequired", true } - }, + { "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, - }, + // 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}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}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}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}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, - }, - }; + 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) + 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) { - // 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) { - if (property.IsSensitive) - { - property.Value = null; - property.View = "sensitivevalue"; - property.Readonly = true; - } + 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 5d4d9ba485bf..cb77d790cdda 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.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) - { - _propertyEditors = propertyEditors; - _dataTypeService = dataTypeService; - _shortStringHelper = shortStringHelper; - _logger = logger; - } + public IEnumerable> Map(IContentTypeComposition source) + { + // deal with groups + var groups = new List>(); - /// - /// 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 local to this content type + foreach (PropertyGroup propertyGroup in source.PropertyGroups) { - // 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); - } + 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, + }; - /// - /// 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); + groups.Add(group); } - public IEnumerable> Map(IContentTypeComposition source) + // add groups inherited through composition + var localGroupIds = groups.Select(x => x.Id).ToArray(); + foreach (PropertyGroup propertyGroup in source.CompositionPropertyGroups) { - // deal with groups - var groups = new List>(); - - // add groups local to this content type - foreach (var propertyGroup in source.PropertyGroups) + // skip those that are local to this content type + if (localGroupIds.Contains(propertyGroup.Id)) { - 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); + continue; } - // add groups inherited through composition - var localGroupIds = groups.Select(x => x.Id).ToArray(); - foreach (var propertyGroup in source.CompositionPropertyGroups) + // get the content type that defines this group + IContentTypeComposition? definingContentType = GetContentTypeForPropertyGroup(source, propertyGroup.Id); + if (definingContentType == null) { - // skip those that are local to this content type - if (localGroupIds.Contains(propertyGroup.Id)) continue; + throw new Exception("PropertyGroup with id=" + propertyGroup.Id + + " was not found on any of the content type's compositions."); + } - // 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 }, + }; - 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); - } + groups.Add(group); + } - // deal with generic properties - var genericProperties = new List(); + // 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 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(); - 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) + // 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) { - 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)); + throw new Exception("PropertyType with id=" + compositionGenericProperty.Id + + " was not found on any of the content type's compositions."); } - // 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); - } + genericProperties.AddRange(MapProperties(new[] { compositionGenericProperty }, definingContentType, PropertyGroupBasic.GenericPropertiesGroupId, true)); + } - // 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) + // if there are any generic properties, add the corresponding tab + if (genericProperties.Any()) + { + var genericGroup = new PropertyGroupDisplay { - lockedPropertyAliases.Add(propertyAlias); - } - // lock properties by aliases - foreach (var property in groups.SelectMany(x => x.Properties)) + 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 (TPropertyType property in groups.SelectMany(x => x.Properties)) + { + if (property.Alias is not null) { - if (property.Alias is not null) - { - property.Locked = lockedPropertyAliases.Contains(property.Alias); - } + 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) + // 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) { - // 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(); + groups.Add(groupsByAlias.First()); + continue; } - return groups.OrderBy(x => x.SortOrder); + // 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(); } - private IEnumerable MapProperties(IEnumerable? properties, IContentTypeBase contentType, int groupId, bool inherited) + 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)) { - var mappedProperties = new List(); + return contentType; + } - foreach (var p in properties?.Where(x => x.DataTypeId != 0).OrderBy(x => x.SortOrder) ?? Enumerable.Empty()) - { - var propertyEditorAlias = p.PropertyEditorAlias; - var propertyEditor = _propertyEditors[propertyEditorAlias]; - var dataType = _dataTypeService.GetDataType(p.DataTypeId); + // 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); + } - //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]; - } + /// + /// 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); + } - var config = propertyEditor is null || dataType is null - ? new Dictionary() - : dataType.Editor?.GetConfigurationEditor().ToConfigurationEditor(dataType.Configuration); + private IEnumerable MapProperties( + IEnumerable? properties, + IContentTypeBase contentType, + int groupId, + bool inherited) + { + var mappedProperties = new List(); - 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() - }); + 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]; } - return mappedProperties; + 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 f4715b3a6b3c..148470c706fa 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; + private readonly IPublishedUrlProvider _publishedUrlProvider; - public RedirectUrlMapDefinition(IPublishedUrlProvider publishedUrlProvider) - { - _publishedUrlProvider = publishedUrlProvider; - } + public RedirectUrlMapDefinition(IPublishedUrlProvider publishedUrlProvider) => + _publishedUrlProvider = publishedUrlProvider; - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new ContentRedirectUrl(), Map); - } + 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; - } + // 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; } } diff --git a/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs index b0aaab953750..d5658368474a 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) - { - _entityService = entityService; - _relationService = relationService; - } + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new RelationTypeDisplay(), Map); + mapper.Define((source, context) => new RelationDisplay(), Map); + mapper.Define(Map); + } - public void DefineMaps(IUmbracoMapper mapper) + // 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) { - mapper.Define((source, context) => new RelationTypeDisplay(), Map); - mapper.Define((source, context) => new RelationDisplay(), Map); - mapper.Define(Map); + targetWithIsDependency.IsDependency = source.IsDependency; } - // 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; + target.Key = source.Key; + target.Name = source.Name; + target.ParentObjectType = source.ParentObjectType; + } - 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; + // 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; - target.IsSystemRelationType = source.IsSystemRelationType(); + if (source is IRelationTypeWithIsDependency sourceWithIsDependency) + { + target.IsDependency = sourceWithIsDependency.IsDependency; + } - // 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(); - } + 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; - if (source.ChildObjectType.HasValue) - { - var objType = ObjectTypes.GetUmbracoObjectType(source.ChildObjectType.Value); - target.ChildObjectTypeName = objType.GetFriendlyName(); - } + target.IsSystemRelationType = source.IsSystemRelationType(); + + // Set the "friendly" and entity names for the parent and child object types + if (source.ParentObjectType.HasValue) + { + 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; + UmbracoObjectTypes objType = ObjectTypes.GetUmbracoObjectType(source.ChildObjectType.Value); + target.ChildObjectTypeName = objType.GetFriendlyName(); + } + } - var entities = _relationService.GetEntitiesFromRelation(source); + // 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; - if (entities is not null) - { - target.ParentName = entities.Item1.Name; - target.ChildName = entities.Item2.Name; - } - } + Tuple? entities = _relationService.GetEntitiesFromRelation(source); - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate - private static void Map(RelationTypeSave source, IRelationType target, MapperContext context) + 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 b7bdbccd2634..c64af5ac0a2e 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; - } + private readonly ILocalizedTextService _textService; - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new Section(), Map); + public SectionMapDefinition(ILocalizedTextService textService) => _textService = textService; + + 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 -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 - 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 be4b6bae61b4..42ea05e8f9d5 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)); - } + } - /// - /// Returns a collection of custom generic properties that exist on the generic properties tab - /// - /// - protected virtual IEnumerable GetCustomGenericProperties(IContentBase content) - { - return Enumerable.Empty(); - } + 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)); + } - /// - /// 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); + protected ICultureDictionary CultureDictionary { get; } + protected ILocalizedTextService LocalizedTextService { get; } + protected IEnumerable IgnoreProperties { get; set; } - var customProperties = GetCustomGenericProperties(content); - if (customProperties != null) - { - genericProperties.AddRange(customProperties); - } + /// + /// Returns a collection of custom generic properties that exist on the generic properties tab + /// + /// + protected virtual IEnumerable GetCustomGenericProperties(IContentBase content) => + Enumerable.Empty(); - if (genericProperties.Count > 0) - { - tabs.Add(new Tab - { - Id = 0, - Label = LocalizedTextService.Localize("general", "properties"), - Alias = "Generic properties", - Properties = genericProperties - }); - } + /// + /// 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) + { + genericProperties.AddRange(customProperties); } - /// - /// Maps a list of to a list of - /// - /// - /// - /// - /// - protected virtual List MapProperties(IContentBase content, List properties, MapperContext context) + if (genericProperties.Count > 0) { - return context.MapEnumerable(properties.OrderBy(x => x.PropertyType?.SortOrder)).WhereNotNull().ToList(); + tabs.Add(new Tab + { + 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) - { - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider ?? throw new ArgumentNullException(nameof(contentTypeBaseServiceProvider)); - } + // Property groups only exist on the content type (as it's only used for display purposes) + IContentTypeComposition? contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source); - public virtual IEnumerable> Map(TSource source, MapperContext context) + // 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) { - var tabs = new List>(); + foreach (IGrouping groupsByAlias in groups.GroupBy(x => x.Alias)) + { + var properties = new List(); - // Property groups only exist on the content type (as it's only used for display purposes) - var contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source); + // Merge properties for groups with the same alias + foreach (PropertyGroup group in groupsByAlias) + { + IEnumerable groupProperties = source.GetPropertiesForGroup(group) + .Where(x => IgnoreProperties.Contains(x.Alias) == false); // Skip ignored properties - // 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 (var groupsByAlias in groups.GroupBy(x => x.Alias)) + properties.AddRange(groupProperties); + } + + if (properties.Count == 0 && (!parentAliases?.Contains(groupsByAlias.Key) ?? false)) { - var properties = new List(); - - // 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 - }); + continue; } - } - MapGenericProperties(source, tabs, context); + // Map the properties + List mappedProperties = MapProperties(source, properties, context); - // Activate the first tab, if any - if (tabs.Count > 0) - tabs[0].IsActive = true; + // Add the tab (the first is closest to the content type, e.g. local, then direct composition) + PropertyGroup g = groupsByAlias.First(); - return tabs; + 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; } } diff --git a/src/Umbraco.Core/Models/Mapping/TagMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/TagMapDefinition.cs index 7bd436fa5448..f9c1690c6a46 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 624868f3f435..5afc8bc8d777 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; + private readonly IShortStringHelper _shortStringHelper; - public TemplateMapDefinition(IShortStringHelper shortStringHelper) - { - _shortStringHelper = shortStringHelper; - } + public TemplateMapDefinition(IShortStringHelper shortStringHelper) => _shortStringHelper = shortStringHelper; - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new TemplateDisplay(), Map); - mapper.Define((source, context) => new Template(_shortStringHelper, source.Name, source.Alias), Map); - } + 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 + 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 a2c3fa7f282c..47ed9ec4abff 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.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; - - public UserMapDefinition(ILocalizedTextService textService, IUserService userService, IEntityService entityService, ISectionService sectionService, - AppCaches appCaches, ActionCollection actions, IOptions globalSettings, MediaFileManager mediaFileManager, IShortStringHelper shortStringHelper, - IImageUrlGenerator imageUrlGenerator) - { - _sectionService = sectionService; - _entityService = entityService; - _userService = userService; - _textService = textService; - _actions = actions; - _appCaches = appCaches; - _globalSettings = globalSettings.Value; - _mediaFileManager = mediaFileManager; - _shortStringHelper = shortStringHelper; - _imageUrlGenerator = imageUrlGenerator; - } + _sectionService = sectionService; + _entityService = entityService; + _userService = userService; + _textService = textService; + _actions = actions; + _appCaches = appCaches; + _globalSettings = globalSettings.Value; + _mediaFileManager = mediaFileManager; + _shortStringHelper = shortStringHelper; + _imageUrlGenerator = imageUrlGenerator; + } - public void DefineMaps(IUmbracoMapper mapper) + 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)) { - 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); + throw new NotSupportedException($"{nameof(target)} must be a UserGroup."); } - // mappers + Map(source, ttarget); + } - private static void Map(UserGroupSave source, IUserGroup target, MapperContext context) + // 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) { - if (!(target is UserGroup ttarget)) - throw new NotSupportedException($"{nameof(target)} must be a UserGroup."); - Map(source, ttarget); + target.Id = id; } - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate - private static void Map(UserGroupSave source, UserGroup target) + target.ClearAllowedSections(); + if (source.Sections is not null) { - 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) + // 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.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()); + target.Icon = Constants.Icons.Member; } + } + + private static string? MapContentTypeIcon(IEntitySlim entity) + => entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; - // 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) + private static int GetIntId(object? id) + { + if (id is string strId && + int.TryParse(strId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var asInt)) { - 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()); + return asInt; } - // Umbraco.Code.MapAll - private static void Map(IProfile source, ContentEditing.UserProfile target, MapperContext context) + Attempt result = id.TryConvertTo(); + if (result.Success == false) { - target.Name = source.Name; - target.UserId = source.Id; + throw new InvalidOperationException( + "Cannot convert the profile to a " + typeof(UserDetail).Name + + " object since the id is not an integer"); } - // 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) + 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.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); + target.AddGroup(group.ToReadOnlyGroup()); } + } - // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Sections -Notifications - // Umbraco.Code.MapAll -Udi -Trashed -AdditionalData -IsSystemUserGroup - private void Map(IUserGroup source, UserGroupBasic target, MapperContext context) + // 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.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); + 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) + // 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.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; + target.Icon = Constants.Icons.UserGroup; } + } - // Umbraco.Code.MapAll -Trashed -Alias -AssignedPermissions - private static void Map(EntitySlim source, AssignedContentPermissions target, MapperContext context) + // 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) { - 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; + contentEntities = Array.Empty(); } - - // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Sections -Notifications -Udi - // Umbraco.Code.MapAll -Trashed -AdditionalData -Users -AssignedPermissions - private void Map(IUserGroup source, UserGroupDisplay target, MapperContext context) + else { - 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) + // 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)) { - contentEntities = Array.Empty(); + list.AddRange(_entityService.GetAll(UmbracoObjectTypes.Document, idGroup.ToArray())); } - else + + 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) { - // 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(); + continue; } - var allAssignedPermissions = new List(); - foreach (var entity in contentEntities) + 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)) { - 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); + permission.Checked = false; + permission.Checked = + contentPermissions.AssignedPermissions.Contains( + permission.PermissionCode, + StringComparer.InvariantCulture); } - target.AssignedPermissions = allAssignedPermissions; + allAssignedPermissions.Add(assignedContentPermissions); } - // 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; - } + 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 -Notifications -IsCurrentUser -Udi -Icon -Trashed -Alias -AdditionalData - private void Map(IUser source, UserBasic target, MapperContext context) + // 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) { - //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; + target.MediaStartNode = + context.Map(_entityService.Get(sourceStartMediaId.Value, UmbracoObjectTypes.Media)); } - - // Umbraco.Code.MapAll -SecondsUntilTimeout - private void Map(IUser source, UserDetail target, MapperContext context) + else if (sourceStartMediaId == -1) { - 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(); + target.MediaStartNode = CreateRootNode(_textService.Localize("media", "mediaRoot")); } - // helpers - - private void MapUserGroupBasic(UserGroupBasic target, IEnumerable sourceAllowedSections, int? sourceStartContentId, int? sourceStartMediaId, MapperContext context) + if (sourceStartContentId > 0) { - 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; + target.ContentStartNode = + context.Map(_entityService.Get(sourceStartContentId.Value, UmbracoObjectTypes.Document)); } - - private IDictionary> MapUserGroupDefaultPermissions(IUserGroup source) + else if (sourceStartContentId == -1) { - 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()); + target.ContentStartNode = CreateRootNode(_textService.Localize("content", "contentRoot")); } - 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 (target.Icon.IsNullOrWhiteSpace()) { - 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; + target.Icon = Constants.Icons.UserGroup; } + } - private IEnumerable CreateUserEditorNavigation() + private IDictionary> MapUserGroupDefaultPermissions(IUserGroup source) + { + Permission GetPermission(IAction action) { - return new[] + return new() { - new EditorNavigation - { - Active = true, - Alias = "details", - Icon = "icon-umb-users", - Name = _textService.Localize("general","user"), - View = "views/users/views/user/details.html" - } + 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), }; } - private static int GetIntId(object? id) + 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) { - 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; + return Enumerable.Empty(); } - private EntityBasic CreateRootNode(string name) + var startNodes = new List(); + if (startNodeIds.Contains(-1)) { - return new EntityBasic - { - Name = name, - Path = "-1", - Icon = "icon-folder", - Id = -1, - Trashed = false, - ParentId = -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 926fe2ef0966..d0cf05b8b982 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) => 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)); - /// - /// Changes the for the current Media object - /// - /// New MediaType for this Media - /// Leaves PropertyTypes intact after change - internal void ChangeContentType(IMediaType mediaType) + if (clearProperties) { - ChangeContentType(mediaType, false); + Properties.EnsureCleanPropertyTypes(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) + else { - 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.EnsurePropertyTypes(mediaType.CompositionPropertyTypes); } + + 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 236ec9deb73a..ee69c25de48b 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 +namespace Umbraco.Extensions; + +public static class MediaExtensions { - public static class MediaExtensions + /// + /// Gets the URL of a media item. + /// + public static string? GetUrl(this IMedia media, string propertyAlias, MediaUrlGeneratorCollection mediaUrlGenerators) { - /// - /// 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)) { - if (media.TryGetMediaPath(propertyAlias, mediaUrlGenerators, out var mediaPath)) - { - return mediaPath; - } - - return string.Empty; + 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 a529dc318999..64683ae462a9 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; + + /// + /// 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) + { + } + /// - /// Represents the content type that a object is based on + /// Constuctor for creating a MediaType with the parent as an inherited type. /// - [Serializable] - [DataContract(IsReference = true)] - public class MediaType : ContentTypeCompositionBase, IMediaType + /// Use this to ensure inheritance from parent. + public MediaType(IShortStringHelper shortStringHelper, IMediaType parent) + : this(shortStringHelper, parent, string.Empty) { - 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, 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 4244e1ba44dc..cddf04b4fe8c 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; - - /// - /// 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()) - { - IsApproved = true; + IsApproved = true; + + // this cannot be null but can be empty + _rawPasswordValue = string.Empty; + _email = string.Empty; + _username = string.Empty; + } - // this cannot be null but can be empty - _rawPasswordValue = ""; - _email = ""; - _username = ""; + /// + /// 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) + { + 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)); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); + } - IsApproved = true; + IsApproved = true; - // this cannot be null but can be empty - _rawPasswordValue = ""; - _email = ""; - _username = ""; + // 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) + { + 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, 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; - 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)); } - /// - /// 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 (email == 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; - CreatorId = userId; - IsApproved = isApproved; - - //this cannot be null but can be empty - _rawPasswordValue = ""; + throw new ArgumentNullException(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) - : base(name, -1, contentType, new PropertyCollection()) + if (string.IsNullOrWhiteSpace(email)) { - _email = email; - _username = username; - _rawPasswordValue = rawPasswordValue; - IsApproved = true; + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + 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 (username == null) { - _email = email; - _username = username; - _rawPasswordValue = rawPasswordValue; - IsApproved = isApproved; + throw new ArgumentNullException(nameof(username)); } - /// - /// 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 (string.IsNullOrWhiteSpace(username)) { - _email = email; - _username = username; - _rawPasswordValue = rawPasswordValue; - IsApproved = isApproved; - CreatorId = userId; + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(username)); } - /// - /// Gets or sets the Username - /// - [DataMember] - public string Username + _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 => _username; - set => SetPropertyValueAndDetectChanges(value, ref _username!, nameof(Username)); + throw new ArgumentNullException(nameof(name)); } - /// - /// Gets or sets the Email - /// - [DataMember] - public string Email + if (string.IsNullOrWhiteSpace(name)) { - get => _email; - set => SetPropertyValueAndDetectChanges(value, ref _email!, nameof(Email)); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - [DataMember] - public DateTime? EmailConfirmedDate + if (email == null) { - get => _emailConfirmedDate; - set => SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, nameof(EmailConfirmedDate)); + throw new ArgumentNullException(nameof(email)); } - /// - /// Gets or sets the raw password value - /// - [IgnoreDataMember] - public string? RawPasswordValue + if (string.IsNullOrWhiteSpace(email)) { - get => _rawPasswordValue; - set - { - 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)); - } - } + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(email)); } - [IgnoreDataMember] - public string? PasswordConfiguration + if (username == null) { - get => _passwordConfig; - set => SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfiguration)); + throw new ArgumentNullException(nameof(username)); } - /// - /// 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 + if (string.IsNullOrWhiteSpace(username)) { - get - { - var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.Comments, nameof(Comments), default(string)); - if (a.Success == false) - return a.Result; + 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; + } - return Properties[Constants.Conventions.Member.Comments]?.GetValue() == null - ? string.Empty - : Properties[Constants.Conventions.Member.Comments]?.GetValue()?.ToString(); + /// + /// 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) + { + // 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; } - set + else { - if (WarnIfPropertyTypeNotFoundOnSet( - Constants.Conventions.Member.Comments, - nameof(Comments)) == false) - return; - - Properties[Constants.Conventions.Member.Comments]?.SetValue(value); + SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, nameof(RawPasswordValue)); } } + } - /// - /// Gets or sets a value indicating whether the Member is approved - /// - [DataMember] - public bool IsApproved - { - get => _isApproved; - set => SetPropertyValueAndDetectChanges(value, ref _isApproved, nameof(IsApproved)); - } + [IgnoreDataMember] + public string? PasswordConfiguration + { + get => _passwordConfig; + set => SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfiguration)); + } - /// - /// 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)); - } + // 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 sets the date for last login - /// - /// - /// Alias: umbracoMemberLastLogin - /// Part of the standard properties collection. - /// - [DataMember] - public DateTime? LastLoginDate + /// + /// Gets or set the comments for the member + /// + /// + /// Alias: umbracoMemberComments + /// Part of the standard properties collection. + /// + [DataMember] + public string? Comments + { + get { - get => _lastLoginDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastLoginDate, nameof(LastLoginDate)); - } + Attempt a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.Comments, nameof(Comments), default(string)); + if (a.Success == false) + { + return a.Result; + } - /// - /// 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)); + return Properties[Constants.Conventions.Member.Comments]?.GetValue() == null + ? string.Empty + : Properties[Constants.Conventions.Member.Comments]?.GetValue()?.ToString(); } - /// - /// Gets or sets the date for when Member was locked out - /// - /// - /// Alias: umbracoMemberLastLockoutDate - /// Part of the standard properties collection. - /// - [DataMember] - public DateTime? LastLockoutDate + set { - get => _lastLockoutDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastLockoutDate, nameof(LastLockoutDate)); - } + if (WarnIfPropertyTypeNotFoundOnSet( + Constants.Conventions.Member.Comments, + nameof(Comments)) == false) + { + return; + } - /// - /// 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)); + Properties[Constants.Conventions.Member.Comments]?.SetValue(value); } + } - /// - /// 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)); - } + /// + /// 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 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) + /// + /// 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("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); - } + 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); + } - // if the property doesn't exist, - if (Properties.Contains(propertyAlias) == false) + // 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) { - // 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); + DoLog(propertyAlias, propertyName); } - return Attempt.Succeed(); + return Attempt.Fail(defaultVal); } - private bool WarnIfPropertyTypeNotFoundOnSet(string propertyAlias, string propertyName) + return Attempt.Succeed(); + } + + private bool WarnIfPropertyTypeNotFoundOnSet(string propertyAlias, string propertyName) + { + 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); - } + 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) + // 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) { - // put a warn in the log if this entity has been persisted - // then return a failure - if (HasIdentity) - DoLog(propertyAlias, propertyName); - return false; + DoLog(propertyAlias, propertyName); } - return true; + return false; } - /// - [DataMember] - [DoNotClone] - public IDictionary? AdditionalData => _additionalData ?? (_additionalData = new Dictionary()); - - /// - [IgnoreDataMember] - public bool HasAdditionalData => _additionalData != null; + return true; } } diff --git a/src/Umbraco.Core/Models/MemberGroup.cs b/src/Umbraco.Core/Models/MemberGroup.cs index 7a35b78875fd..5ae7a7edd224 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 string? _name; - private int _creatorId; + private IDictionary? _additionalData; + private int _creatorId; + private string? _name; - /// - [DataMember] - [DoNotClone] - public IDictionary AdditionalData => _additionalData ?? (_additionalData = new Dictionary()); + /// + [DataMember] + [DoNotClone] + public IDictionary AdditionalData => +_additionalData ??= new Dictionary(); - /// - [IgnoreDataMember] - public bool HasAdditionalData => _additionalData != null; + /// + [IgnoreDataMember] + public bool HasAdditionalData => _additionalData != null; - [DataMember] - public string? Name + [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 f6d06956e50e..96466af39747 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 -{ - /// - /// A simple representation of an Umbraco member property - /// - public class MemberPropertyModel - { - [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. +namespace Umbraco.Cms.Core.Models; - [DataType(System.ComponentModel.DataAnnotations.DataType.Text)] - public string? Value { get; set; } +/// +/// A simple representation of an Umbraco member property +/// +public class MemberPropertyModel +{ + [Required] + public string Alias { get; set; } = null!; - [ReadOnly(true)] - public string? Name { get; set; } + // 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; } - // TODO: Perhaps one day we'll ship with our own EditorTempates but for now developers can just render their own inside the view + [ReadOnly(true)] + public string? Name { 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; } + // 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; } } diff --git a/src/Umbraco.Core/Models/MemberType.cs b/src/Umbraco.Core/Models/MemberType.cs index 4db8388b9437..502a61df9f4a 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) - { - _shortStringHelper = shortStringHelper; - _memberTypePropertyTypes = new Dictionary(); - } + public MemberType(IShortStringHelper shortStringHelper, IContentTypeComposition parent, string alias) + : base(shortStringHelper, parent, alias) + { + _shortStringHelper = shortStringHelper; + _memberTypePropertyTypes = new Dictionary(); + } - public MemberType(IShortStringHelper shortStringHelper, IContentTypeComposition parent) : this(shortStringHelper, parent, string.Empty) - { - } + /// + 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); - public MemberType(IShortStringHelper shortStringHelper, IContentTypeComposition parent, string alias) - : base(shortStringHelper, parent, alias) + /// + /// 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 override ISimpleContentType ToSimple() => new SimpleContentType(this); - - /// - public override bool SupportsPublishing => SupportsPublishingConst; + /// + /// 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; - 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. + /// + /// 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; - get => base.Variations; - set => throw new NotSupportedException("Variations are not supported on members."); - } + /// + /// 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; - /// - /// The Alias of the ContentType - /// - [DataMember] - public override string Alias + /// + /// 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)) { - 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)); - } + propertyProfile.IsEditable = value; } - - /// - /// 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) + else { - return propertyTypeAlias is not null && _memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile) && propertyProfile.IsEditable; + var tuple = new MemberTypePropertyProfileAccess(false, value, false); + _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); } + } - /// - /// 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) - { - 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) + /// + /// 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)) { - return _memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile) && propertyProfile.IsSensitive; + propertyProfile.IsVisible = 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 - public void SetMemberCanEditProperty(string propertyTypeAlias, bool value) + else { - if (_memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile)) - { - propertyProfile.IsEditable = value; - } - else - { - var tuple = new MemberTypePropertyProfileAccess(false, value, false); - _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); - } + var tuple = new MemberTypePropertyProfileAccess(value, false, 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) + /// + /// 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)) { - if (_memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile)) - { - propertyProfile.IsVisible = value; - } - else - { - var tuple = new MemberTypePropertyProfileAccess(value, false, false); - _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); - } + propertyProfile.IsSensitive = 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 - public void SetIsSensitiveProperty(string propertyTypeAlias, bool value) + else { - 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 89bf2f283dd4..e6e619354b97 100644 --- a/src/Umbraco.Core/Models/MemberTypePropertyProfileAccess.cs +++ b/src/Umbraco.Core/Models/MemberTypePropertyProfileAccess.cs @@ -1,19 +1,20 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Used to track the property types that are visible/editable on member profiles +/// +public class MemberTypePropertyProfileAccess { - /// - /// Used to track the property types that are visible/editable on member profiles - /// - public class MemberTypePropertyProfileAccess + public MemberTypePropertyProfileAccess(bool isVisible, bool isEditable, bool isSenstive) { - 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; } + 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 9c585589fac0..613a873d7a4b 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; + private readonly IContent _content; - public ContentPermissionSet(IContent content, EntityPermissionCollection permissionsSet) - : base(content.Id, permissionsSet) - { - _content = content; - } + public ContentPermissionSet(IContent content, EntityPermissionCollection permissionsSet) + : base(content.Id, permissionsSet) => + _content = content; - public override int EntityId - { - get { return _content.Id; } - } + public override int EntityId => _content.Id; - #region Explicit implementation of IAggregateRoot - int IEntity.Id - { - get { return EntityId; } - set { throw new NotImplementedException(); } - } + int IEntity.Id + { + get => EntityId; + set => throw new NotImplementedException(); + } - bool IEntity.HasIdentity - { - get { return EntityId > 0; } - } + bool IEntity.HasIdentity => EntityId > 0; - void IEntity.ResetIdentity() => throw new InvalidOperationException($"Resetting identity on {nameof(ContentPermissionSet)} is invalid"); + Guid IEntity.Key { get; set; } - 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.CreateDate { get; set; } - DateTime IEntity.UpdateDate { get; set; } + DateTime IEntity.UpdateDate { get; set; } - DateTime? IEntity.DeleteDate { get; set; } + DateTime? IEntity.DeleteDate { get; set; } - object IDeepCloneable.DeepClone() - { - throw new NotImplementedException(); - } - #endregion - } + 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 a86c844622cb..58e84f27f907 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 { + 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; } + + public int UserGroupId { get; } + + /// + /// The assigned permissions for the user/entity combo + /// + public string[] AssignedPermissions { get; } + /// - /// Represents an entity permission (defined on the user group and derived to retrieve permissions for a given user) + /// True if the permissions assigned to this object are the group's default permissions and not explicitly defined + /// permissions /// - public class EntityPermission : IEquatable + /// + /// 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) { - public EntityPermission(int groupId, int entityId, string[] assignedPermissions) + if (ReferenceEquals(null, other)) { - UserGroupId = groupId; - EntityId = entityId; - AssignedPermissions = assignedPermissions; - IsDefaultPermissions = false; + return false; } - public EntityPermission(int groupId, int entityId, string[] assignedPermissions, bool isDefaultPermissions) + if (ReferenceEquals(this, other)) { - UserGroupId = groupId; - EntityId = entityId; - AssignedPermissions = assignedPermissions; - IsDefaultPermissions = isDefaultPermissions; + return true; } - 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; } + return EntityId == other.EntityId && UserGroupId == other.UserGroupId; + } - public bool Equals(EntityPermission? other) + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return EntityId == other.EntityId && UserGroupId == other.UserGroupId; + 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((EntityPermission) obj); + return true; } - public override int GetHashCode() + if (obj.GetType() != GetType()) { - unchecked - { - return (EntityId * 397) ^ UserGroupId; - } + 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 ac03ef75d8f1..727f7964f771 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) - { - } + 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) + /// + /// 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) { - 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; + _aggregateNodePermissions = new Dictionary(); } - private Dictionary? _aggregateNodePermissions; - - /// - /// Returns the aggregate permissions in the permission set for all nodes - /// - /// - /// - /// This value is only calculated once - /// - public IEnumerable GetAllPermissions() + if (_aggregateNodePermissions.TryGetValue(entityId, out string[]? entityPermissions) == false) { - return _aggregatePermissions ?? (_aggregatePermissions = - this.SelectMany(x => x.AssignedPermissions).Distinct().ToArray()); + entityPermissions = this.Where(x => x.EntityId == entityId).SelectMany(x => x.AssignedPermissions) + .Distinct().ToArray(); + _aggregateNodePermissions[entityId] = entityPermissions; } - private string[]? _aggregatePermissions; + 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 68e97a5d9fa7..0ae0dbf33573 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 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; } + private static readonly Lazy EmptyInstance = + new(() => new EntityPermissionSet(-1, new EntityPermissionCollection())); - /// - /// 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(); - } + public EntityPermissionSet(int entityId, EntityPermissionCollection permissionsSet) + { + 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 f8efe5588524..704158a1af8e 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; } + + /// + /// 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; } + + string? Comments { get; set; } + + bool IsApproved { get; set; } + + bool IsLockedOut { get; set; } + + DateTime? LastLoginDate { get; set; } + + DateTime? LastPasswordChangeDate { get; set; } + + DateTime? LastLockoutDate { get; set; } + /// - /// Defines the base contract for and + /// Gets or sets the number of failed password attempts. + /// This is the number of times the password was entered incorrectly upon login. /// - public interface IMembershipUser : IEntity - { - string Username { get; set; } - string Email { get; set; } - DateTime? EmailConfirmedDate { 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; } - - string? Comments { get; set; } - bool IsApproved { get; set; } - bool IsLockedOut { get; set; } - DateTime? LastLoginDate { 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; } - } + /// + /// 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 395ebe0de82f..f30bfd122524 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 be84b4bca6eb..2096ec3d67d7 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; } - - /// - /// 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; } - - IEnumerable AllowedSections { get; } - } + 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; } + + IEnumerable AllowedSections { get; } } diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index c7c68dabda0c..6fc409a0c02b 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; } + + /// + /// Gets the groups that user is part of + /// + IEnumerable Groups { get; } + + IEnumerable AllowedSections { get; } + + /// + /// Exposes the basic profile data + /// + IProfile ProfileData { get; } + + /// + /// Will hold the media file system relative path of the users custom avatar if they uploaded one + /// + string? Avatar { get; set; } /// - /// Defines the interface for a + /// A Json blob stored for recording tour data for a user /// - /// Will be left internal until a proper Membership implementation is part of the roadmap - public interface IUser : IMembershipUser, IRememberBeingDirty, ICanBeDirty - { - 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; } - - /// - /// Gets the groups that user is part of - /// - IEnumerable Groups { get; } - - void RemoveGroup(string group); - void ClearGroups(); - void AddGroup(IReadOnlyUserGroup group); - - IEnumerable AllowedSections { get; } - - /// - /// Exposes the basic profile data - /// - IProfile ProfileData { get; } - - /// - /// 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; } - } + string? TourData { get; set; } + + void RemoveGroup(string group); + + void ClearGroups(); + + void AddGroup(IReadOnlyUserGroup group); } diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs index 96ae3c6dfb23..71ef6a7a123d 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? StartContentId { get; set; } - int? StartMediaId { get; set; } + int? StartMediaId { get; set; } - /// - /// The icon - /// - string? Icon { get; set; } + /// + /// The icon + /// + string? Icon { get; set; } - /// - /// The name - /// - string? Name { get; set; } + /// + /// The name + /// + string? Name { 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 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; } - void RemoveAllowedSection(string sectionAlias); + /// + /// Specifies the number of users assigned to this group + /// + int UserCount { get; } - void AddAllowedSection(string sectionAlias); + void RemoveAllowedSection(string sectionAlias); - void ClearAllowedSections(); + void AddAllowedSection(string sectionAlias); - /// - /// Specifies the number of users assigned to this group - /// - int UserCount { get; } - } + void ClearAllowedSections(); } diff --git a/src/Umbraco.Core/Models/Membership/MemberCountType.cs b/src/Umbraco.Core/Models/Membership/MemberCountType.cs index 89990994e812..6ff29bdee2ee 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 fec933190cc0..a34f1a8d1dc6 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 3e4831d9c337..f1c0463bdd7b 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 24543337baea..2e32f4172bcc 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(); - - //Zero is invalid and will be treated as Null - StartContentId = startContentId == 0 ? null : startContentId; - StartMediaId = startMediaId == 0 ? null : startMediaId; - } + 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; } - 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) + public bool Equals(ReadOnlyUserGroup? 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((ReadOnlyUserGroup) obj); + return true; } - public override int GetHashCode() + 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 Alias?.GetHashCode() ?? base.GetHashCode(); + return false; } - public static bool operator ==(ReadOnlyUserGroup left, ReadOnlyUserGroup right) + if (ReferenceEquals(this, obj)) { - return Equals(left, right); + return true; } - public static bool operator !=(ReadOnlyUserGroup left, ReadOnlyUserGroup right) + if (obj.GetType() != GetType()) { - return !Equals(left, right); + 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 463b44c73e10..4607b7c81184 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 = ""; - _username = string.Empty; - _email = string.Empty; - _name = string.Empty; - } + 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) + /// + /// 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)) { - 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(name)); } - /// - /// 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(email)) { - //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(email)); } - 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.IsNullOrWhiteSpace(username)) { - get => _emailConfirmedDate; - set => SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, nameof(EmailConfirmedDate)); + throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); } - [DataMember] - public DateTime? InvitedDate + if (string.IsNullOrEmpty(rawPasswordValue)) { - get => _invitedDate; - set => SetPropertyValueAndDetectChanges(value, ref _invitedDate, nameof(InvitedDate)); + throw new ArgumentException("Value cannot be null or empty.", nameof(rawPasswordValue)); } - [DataMember] - public string Username - { - get => _username; - set => SetPropertyValueAndDetectChanges(value, ref _username!, nameof(Username)); - } + _name = name; + _email = email; + _username = username; + _rawPasswordValue = rawPasswordValue; + _userGroups = new HashSet(); + _isApproved = true; + _isLockedOut = false; + _startContentIds = new int[] { }; + _startMediaIds = new int[] { }; + } - [DataMember] - public string 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) + { + // we allow whitespace for this value so just check null + if (rawPasswordValue == null) { - get => _email; - set => SetPropertyValueAndDetectChanges(value, ref _email!, nameof(Email)); + throw new ArgumentNullException(nameof(rawPasswordValue)); } - [IgnoreDataMember] - public string? RawPasswordValue + if (userGroups == null) { - get => _rawPasswordValue; - set => SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, nameof(RawPasswordValue)); + throw new ArgumentNullException(nameof(userGroups)); } - [IgnoreDataMember] - public string? PasswordConfiguration + if (string.IsNullOrWhiteSpace(name)) { - get => _passwordConfig; - set => SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfiguration)); + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); } - [DataMember] - public bool IsApproved + if (string.IsNullOrWhiteSpace(username)) { - get => _isApproved; - set => SetPropertyValueAndDetectChanges(value, ref _isApproved, nameof(IsApproved)); + throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); } - [IgnoreDataMember] - public bool IsLockedOut - { - get => _isLockedOut; - set => SetPropertyValueAndDetectChanges(value, ref _isLockedOut, nameof(IsLockedOut)); - } + 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)); + } - [IgnoreDataMember] - public DateTime? LastLoginDate - { - get => _lastLoginDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastLoginDate, nameof(LastLoginDate)); - } + [DataMember] + public DateTime? EmailConfirmedDate + { + get => _emailConfirmedDate; + set => SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, nameof(EmailConfirmedDate)); + } - [IgnoreDataMember] - public DateTime? LastPasswordChangeDate - { - get => _lastPasswordChangedDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangedDate, nameof(LastPasswordChangeDate)); - } + [DataMember] + public DateTime? InvitedDate + { + get => _invitedDate; + set => SetPropertyValueAndDetectChanges(value, ref _invitedDate, nameof(InvitedDate)); + } - [IgnoreDataMember] - public DateTime? LastLockoutDate - { - get => _lastLockoutDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastLockoutDate, nameof(LastLockoutDate)); - } + [DataMember] + public string Username + { + get => _username; + set => SetPropertyValueAndDetectChanges(value, ref _username!, nameof(Username)); + } - [IgnoreDataMember] - public int FailedPasswordAttempts - { - get => _failedLoginAttempts; - set => SetPropertyValueAndDetectChanges(value, ref _failedLoginAttempts, nameof(FailedPasswordAttempts)); - } + [DataMember] + public string Email + { + get => _email; + set => SetPropertyValueAndDetectChanges(value, ref _email!, nameof(Email)); + } - [IgnoreDataMember] - public string? Comments { get; set; } + [IgnoreDataMember] + public string? RawPasswordValue + { + get => _rawPasswordValue; + set => SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, nameof(RawPasswordValue)); + } - public UserState UserState + [IgnoreDataMember] + public string? PasswordConfiguration + { + get => _passwordConfig; + set => SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfiguration)); + } + + [DataMember] + public bool IsApproved + { + get => _isApproved; + set => SetPropertyValueAndDetectChanges(value, ref _isApproved, nameof(IsApproved)); + } + + [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; + return UserState.Invited; + } - if (IsLockedOut) - return UserState.LockedOut; - if (IsApproved == false) - return UserState.Disabled; + if (IsLockedOut) + { + return UserState.LockedOut; + } - // User is not disabled or locked and has never logged in before - if (LastLoginDate == default && IsApproved && IsLockedOut == false) - return UserState.Inactive; + if (IsApproved == false) + { + return UserState.Disabled; + } - return UserState.Active; + // User is not disabled or locked and has never logged in before + if (LastLoginDate == default && IsApproved && IsLockedOut == false) + { + return UserState.Inactive; } - } - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + return UserState.Active; } + } - public IEnumerable AllowedSections - { - get { return _allowedSections ?? (_allowedSections = new List(_userGroups.SelectMany(x => x.AllowedSections).Distinct())); } - } + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + } - public IProfile ProfileData => new WrappedUserProfile(this); + public IEnumerable AllowedSections => _allowedSections ??= new List(_userGroups + .SelectMany(x => x.AllowedSections).Distinct()); - /// - /// The security stamp used by ASP.Net identity - /// - [IgnoreDataMember] - public string? SecurityStamp - { - get => _securityStamp; - set => SetPropertyValueAndDetectChanges(value, ref _securityStamp, nameof(SecurityStamp)); - } + public IProfile ProfileData => new WrappedUserProfile(this); - [DataMember] - public string? Avatar - { - get => _avatar; - set => SetPropertyValueAndDetectChanges(value, ref _avatar, nameof(Avatar)); - } + /// + /// The security stamp used by ASP.Net identity + /// + [IgnoreDataMember] + public string? SecurityStamp + { + get => _securityStamp; + set => SetPropertyValueAndDetectChanges(value, ref _securityStamp, nameof(SecurityStamp)); + } - /// - /// A Json blob stored for recording tour data for a user - /// - [DataMember] - public string? TourData - { - get => _tourData; - set => SetPropertyValueAndDetectChanges(value, ref _tourData, nameof(TourData)); - } + [DataMember] + public string? Avatar + { + get => _avatar; + set => SetPropertyValueAndDetectChanges(value, ref _avatar, nameof(Avatar)); + } - /// - /// Gets or sets the session timeout. - /// - /// - /// The session timeout. - /// - [DataMember] - public int SessionTimeout - { - get => _sessionTimeout; - set => SetPropertyValueAndDetectChanges(value, ref _sessionTimeout, nameof(SessionTimeout)); - } + /// + /// 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 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 session timeout. + /// + /// + /// The session timeout. + /// + [DataMember] + public int SessionTimeout + { + get => _sessionTimeout; + set => SetPropertyValueAndDetectChanges(value, ref _sessionTimeout, nameof(SessionTimeout)); + } - /// - /// 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); - } + /// + /// 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); + } - [DataMember] - public string? Language - { - get => _language; - set => SetPropertyValueAndDetectChanges(value, ref _language, nameof(Language)); - } + /// + /// 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); + } - /// - /// Gets the groups that user is part of - /// - [DataMember] - public IEnumerable Groups => _userGroups; + [DataMember] + public string? Language + { + get => _language; + set => SetPropertyValueAndDetectChanges(value, ref _language, nameof(Language)); + } - public void RemoveGroup(string group) - { - foreach (var 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)); - } - } - } + /// + /// Gets the groups that user is part of + /// + [DataMember] + public IEnumerable Groups => _userGroups; - public void ClearGroups() + public void RemoveGroup(string group) + { + foreach (IReadOnlyUserGroup userGroup in _userGroups.ToArray()) { - if (_userGroups.Count > 0) + if (userGroup.Alias == group) { - _userGroups.Clear(); - //reset this flag so it's rebuilt with the assigned groups + _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)) - { - //reset this flag so it's rebuilt with the assigned groups - _allowedSections = null; - OnPropertyChanged(nameof(Groups)); - } + _userGroups.Clear(); + + // reset this flag so it's rebuilt with the assigned groups + _allowedSections = null; + OnPropertyChanged(nameof(Groups)); } + } - protected override void PerformDeepClone(object clone) + public void AddGroup(IReadOnlyUserGroup group) + { + if (_userGroups.Add(group)) { - base.PerformDeepClone(clone); + // reset this flag so it's rebuilt with the assigned groups + _allowedSections = null; + OnPropertyChanged(nameof(Groups)); + } + } - var clonedEntity = (User)clone; + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(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; + var clonedEntity = (User)clone; - } + // manually clone the start node props + clonedEntity._startContentIds = _startContentIds?.ToArray(); + clonedEntity._startMediaIds = _startMediaIds?.ToArray(); - /// - /// Internal class used to wrap the user in a profile - /// - private class WrappedUserProfile : IProfile - { - private readonly IUser _user; + // 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; + } - public WrappedUserProfile(IUser user) - { - _user = user; - } + /// + /// Internal class used to wrap the user in a profile + /// + private class WrappedUserProfile : IProfile + { + private readonly IUser _user; - public int Id => _user.Id; + public WrappedUserProfile(IUser user) => _user = user; - public string? Name => _user.Name; + public int Id => _user.Id; - private bool Equals(WrappedUserProfile other) + public string? Name => _user.Name; + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - return _user.Equals(other._user); + 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((WrappedUserProfile) obj); + return true; } - public override int GetHashCode() + if (obj.GetType() != GetType()) { - return _user.GetHashCode(); + return false; } + + return Equals((WrappedUserProfile)obj); } + 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 5807a83abec2..fcc12912cc0b 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; - - //Custom comparer for enumerable - private static readonly DelegateEqualityComparer> StringEnumerableComparer = - new DelegateEqualityComparer>( - (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), - enum1 => enum1.GetHashCode()); - - /// - /// Constructor to create a new user group - /// - public UserGroup(IShortStringHelper shortStringHelper) - { - _alias = string.Empty; - _name = string.Empty; - _shortStringHelper = shortStringHelper; - _sectionCollection = new List(); - } + _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; - } + /// + /// 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? 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 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? 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 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)); - } + [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); - } + /// + /// 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 IEnumerable AllowedSections => _sectionCollection; - public void RemoveAllowedSection(string sectionAlias) - { - if (_sectionCollection.Contains(sectionAlias)) - _sectionCollection.Remove(sectionAlias); - } + public int UserCount { get; } - public void AddAllowedSection(string sectionAlias) + public void RemoveAllowedSection(string sectionAlias) + { + if (_sectionCollection.Contains(sectionAlias)) { - if (_sectionCollection.Contains(sectionAlias) == false) - _sectionCollection.Add(sectionAlias); + _sectionCollection.Remove(sectionAlias); } + } - public void ClearAllowedSections() + public void AddAllowedSection(string sectionAlias) + { + if (_sectionCollection.Contains(sectionAlias) == false) { - _sectionCollection.Clear(); + _sectionCollection.Add(sectionAlias); } + } - public int UserCount { get; } - - protected override void PerformDeepClone(object clone) - { + public void ClearAllowedSections() => _sectionCollection.Clear(); - base.PerformDeepClone(clone); + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); - var clonedEntity = (UserGroup)clone; + var clonedEntity = (UserGroup)clone; - //manually clone the start node props - clonedEntity._sectionCollection = new List(_sectionCollection); - } + // 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 84b165b81e7c..d71c7aa4ce5c 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); + // 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 IReadOnlyUserGroup group) => - IsSystemUserGroup(group.Alias); + public static bool IsSystemUserGroup(this IUserGroup group) => + IsSystemUserGroup(group.Alias); - private static bool IsSystemUserGroup(this string? groupAlias) - { - return groupAlias == Constants.Security.AdminGroupAlias - || groupAlias == Constants.Security.SensitiveDataGroupAlias - || groupAlias == Constants.Security.TranslatorGroupAlias; - } - } + 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 aca757b317e2..51eb882a6b16 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; - } + Id = id; + Name = name; + } + + public int Id { get; } + + public string? Name { get; } + + public static bool operator ==(UserProfile left, UserProfile right) => Equals(left, right); - public int Id { get; private set; } - public string? Name { get; private set; } + public static bool operator !=(UserProfile left, UserProfile right) => Equals(left, right) == false; - public bool Equals(UserProfile? other) + public bool Equals(UserProfile? other) + { + if (ReferenceEquals(null, other)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Id == other.Id; + 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((UserProfile) obj); + return true; } - public override int GetHashCode() + return Id == other.Id; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - return Id; + return false; } - public static bool operator ==(UserProfile left, UserProfile right) + if (ReferenceEquals(this, obj)) { - return Equals(left, right); + return true; } - public static bool operator !=(UserProfile left, UserProfile right) + if (obj.GetType() != GetType()) { - return Equals(left, right) == false; + 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 13d20771050b..e59e4d25c854 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 f62dc7eb6030..ab1294b13efe 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 - { - public MigrationEntry() - { - } + private string? _migrationName; + private SemVersion? _version; - public MigrationEntry(int id, DateTime createDate, string migrationName, SemVersion version) - { - Id = id; - CreateDate = createDate; - _migrationName = migrationName; - _version = version; - } + public MigrationEntry() + { + } - private string? _migrationName; - private SemVersion? _version; + public MigrationEntry(int id, DateTime createDate, string migrationName, SemVersion version) + { + Id = id; + CreateDate = createDate; + _migrationName = migrationName; + _version = 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 95091efe1f5c..31d17513a6c6 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 5174ee636b23..85e2cfdcd68c 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 c644f7c1a698..51b1e4031ee8 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 8e4eef3246d1..0f44a269cca4 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 +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(); + 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(); + /// - /// Provides utilities and extension methods to handle object types. + /// Gets the Umbraco object type corresponding to a name. /// - 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(); - - private static FieldInfo? GetEnumField(string name) - { - return typeof (UmbracoObjectTypes).GetField(name, BindingFlags.Public | BindingFlags.Static); - } + 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) + private static FieldInfo? GetEnumField(Guid guid) + { + FieldInfo[] fields = typeof(UmbracoObjectTypes).GetFields(BindingFlags.Public | BindingFlags.Static); + foreach (FieldInfo field in fields) { - var fields = typeof (UmbracoObjectTypes).GetFields(BindingFlags.Public | BindingFlags.Static); - foreach (var field in fields) + UmbracoObjectTypeAttribute? attribute = field.GetCustomAttribute(false); + if (attribute != null && attribute.ObjectId == guid) { - var attribute = field.GetCustomAttribute(false); - if (attribute != null && attribute.ObjectId == guid) return field; + return field; } - - return null; } - /// - /// Gets the Umbraco object type corresponding to a name. - /// - public static UmbracoObjectTypes GetUmbracoObjectType(string name) - { - return (UmbracoObjectTypes) Enum.Parse(typeof (UmbracoObjectTypes), name, true); - } + return null; + } - #region Guid object type utilities + #region Guid object type utilities - /// - /// Gets the Umbraco object type corresponding to an object type Guid. - /// - public static UmbracoObjectTypes GetUmbracoObjectType(Guid objectType) + /// + /// Gets the Umbraco object type corresponding to an object type Guid. + /// + public static UmbracoObjectTypes GetUmbracoObjectType(Guid objectType) => + GuidObjectTypes.GetOrAdd(objectType, t => { - return GuidObjectTypes.GetOrAdd(objectType, t => + FieldInfo? field = GetEnumField(objectType); + if (field == null) { - var field = GetEnumField(objectType); - if (field == null) return UmbracoObjectTypes.Unknown; + return UmbracoObjectTypes.Unknown; + } - return (UmbracoObjectTypes?)field.GetValue(null) ?? UmbracoObjectTypes.Unknown; - }); - } + return (UmbracoObjectTypes?)field.GetValue(null) ?? UmbracoObjectTypes.Unknown; + }); - /// - /// Gets the Udi type corresponding to an object type Guid. - /// - public static string GetUdiType(Guid objectType) + /// + /// Gets the Udi type corresponding to an object type Guid. + /// + public static string GetUdiType(Guid objectType) => + GuidUdiTypes.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 Constants.UdiEntityType.Unknown; + } - var attribute = field.GetCustomAttribute(false); - return attribute?.UdiType ?? Constants.UdiEntityType.Unknown; - }); - } + UmbracoUdiTypeAttribute? attribute = field.GetCustomAttribute(false); + return attribute?.UdiType ?? Constants.UdiEntityType.Unknown; + }); - /// - /// Gets the CLR type corresponding to an object type Guid. - /// - public static Type? GetClrType(Guid objectType) + /// + /// Gets the CLR type corresponding to an object type Guid. + /// + public static Type? GetClrType(Guid objectType) => + GuidTypes.GetOrAdd(objectType, t => { - return GuidTypes.GetOrAdd(objectType, t => + FieldInfo? field = GetEnumField(objectType); + if (field == null) { - var field = GetEnumField(objectType); - if (field == null) return null; + return null; + } - var attribute = field.GetCustomAttribute(false); - return attribute?.ModelType; - }); - } + UmbracoObjectTypeAttribute? attribute = field.GetCustomAttribute(false); + return attribute?.ModelType; + }); - #endregion + #endregion - #region UmbracoObjectTypes extension methods + #region UmbracoObjectTypes extension methods - /// - /// Gets the object type Guid corresponding to this Umbraco object type. - /// - public static Guid GetGuid(this UmbracoObjectTypes objectType) + /// + /// Gets the object type Guid corresponding to this Umbraco object type. + /// + public static Guid GetGuid(this UmbracoObjectTypes objectType) => + UmbracoGuids.GetOrAdd(objectType, t => { - return UmbracoGuids.GetOrAdd(objectType, t => - { - var field = GetEnumField(t.ToString()); - var attribute = field?.GetCustomAttribute(false); + FieldInfo? field = GetEnumField(t.ToString()); + UmbracoObjectTypeAttribute? attribute = field?.GetCustomAttribute(false); - return attribute?.ObjectId ?? Guid.Empty; - }); - } + return attribute?.ObjectId ?? Guid.Empty; + }); - /// - /// Gets the Udi type corresponding to this Umbraco object type. - /// - public static string GetUdiType(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 UmbracoUdiTypes.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?.UdiType ?? Constants.UdiEntityType.Unknown; - }); - } + return attribute?.UdiType ?? Constants.UdiEntityType.Unknown; + }); - /// - /// Gets the name corresponding to this Umbraco object type. - /// - public static string? GetName(this UmbracoObjectTypes objectType) - { - return Enum.GetName(typeof (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) + /// + /// Gets the friendly name corresponding to this Umbraco object type. + /// + public static string GetFriendlyName(this UmbracoObjectTypes objectType) => + UmbracoFriendlyNames.GetOrAdd(objectType, t => { - return UmbracoFriendlyNames.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?.ToString() ?? string.Empty; - }); - } + return attribute?.ToString() ?? string.Empty; + }); - /// - /// Gets the CLR type corresponding to this Umbraco object type. - /// - public static Type? GetClrType(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 UmbracoTypes.GetOrAdd(objectType, t => - { - var field = GetEnumField(t.ToString()); - var attribute = field?.GetCustomAttribute(false); + FieldInfo? field = GetEnumField(t.ToString()); + UmbracoObjectTypeAttribute? attribute = field?.GetCustomAttribute(false); - return attribute?.ModelType; - }); - } + return attribute?.ModelType; + }); - #endregion - } + #endregion } diff --git a/src/Umbraco.Core/Models/Packaging/CompiledPackage.cs b/src/Umbraco.Core/Models/Packaging/CompiledPackage.cs index e6c430627cf3..6119d2cea170 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 0fb1c609085e..794262406a69 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 string? ImportMode { get; set; } //this is never used + public XElement XmlData { get; set; } = null!; - /// - /// 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 7cad9b5b9a0d..d1154f1b30af 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 f15768cc2d9d..6dbe6dd703ca 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; + + if (pageSize > 0) + { + TotalPages = (long)Math.Ceiling(totalItems / (decimal)pageSize); + } + else { - TotalItems = totalItems; - PageNumber = pageNumber; - PageSize = pageSize; - - if (pageSize > 0) - { - TotalPages = (long)Math.Ceiling(totalItems / (decimal)pageSize); - } - else - { - TotalPages = 1; - } + TotalPages = 1; } + } - [DataMember(Name = "pageNumber")] - public long PageNumber { get; private set; } + [DataMember(Name = "pageNumber")] + public long PageNumber { get; private set; } - [DataMember(Name = "pageSize")] - public long PageSize { get; private set; } + [DataMember(Name = "pageSize")] + public long PageSize { get; private set; } - [DataMember(Name = "totalPages")] - public long TotalPages { get; private set; } + [DataMember(Name = "totalPages")] + public long TotalPages { get; private set; } - [DataMember(Name = "totalItems")] - public long TotalItems { 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() + /// + /// 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) { - if (PageNumber > 0 && PageSize > 0) - { - return Convert.ToInt32((PageNumber - 1) * PageSize); - } - return 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 125256ec3bb9..c2d11a4f826c 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 +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a paged result for a model collection +/// +/// +[DataContract(Name = "pagedCollection", Namespace = "")] +public class PagedResult : PagedResult { - /// - /// 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) { - public PagedResult(long totalItems, long pageNumber, long pageSize) - : base(totalItems, pageNumber, pageSize) - { } - - [DataMember(Name = "items")] - public IEnumerable? Items { get; set; } } + + [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 ffa9412c5195..2900674570f6 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 PartialView(PartialViewType viewType, string path, Func? getFileContent) + : base(path, getFileContent) => + ViewType = viewType; - public PartialViewType ViewType { get; set; } - } + public PartialViewType ViewType { get; set; } } diff --git a/src/Umbraco.Core/Models/PartialViewMacroModel.cs b/src/Umbraco.Core/Models/PartialViewMacroModel.cs index 662894b39faa..0d999d5dd6f3 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) { + Content = page; + MacroParameters = macroParams; + MacroName = macroName; + MacroAlias = macroAlias; + MacroId = macroId; + } - public PartialViewMacroModel(IPublishedContent page, - int macroId, - string? macroAlias, - string? macroName, - IDictionary macroParams) - { - Content = page; - MacroParameters = macroParams; - MacroName = macroName; - MacroAlias = macroAlias; - MacroId = macroId; - } + public string? MacroName { get; } - public IPublishedContent Content { get; } - public string? MacroName { get; } - public string? MacroAlias { get; } - public int MacroId { get; } - public IDictionary MacroParameters { 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 aea801719f69..ecbf22323be3 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 5dc6dbc59cad..65499be9a2c4 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 231940f105f7..0cd405e60432 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 f4bba10c2c98..195772be3a6d 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 -{ - /// - /// Represents a property. - /// - [Serializable] - [DataContract(IsReference = true)] - public class Property : EntityBase, IProperty - { - // _values contains all property values, including the invariant-neutral value - private List _values = new List(); - - // _pvalue contains the invariant-neutral property value - private IPropertyValue? _pvalue; +namespace Umbraco.Cms.Core.Models; - // _vvalues contains the (indexed) variant property values - private Dictionary? _vvalues; - - /// - /// Initializes a new instance of the class. - /// - public Property(IPropertyType propertyType) - { - PropertyType = propertyType; - } - - /// - /// Initializes a new instance of the class. - /// - public Property(int id, IPropertyType propertyType) +/// +/// Represents a property. +/// +[Serializable] +[DataContract(IsReference = true)] +public class Property : EntityBase, IProperty +{ + private static readonly DelegateEqualityComparer PropertyValueComparer = new( + (o, o1) => { - Id = id; - PropertyType = propertyType; - } + if (o == null && o1 == null) + { + return true; + } - /// - /// 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 + // custom comparer for strings. + // if one is null and another is empty then they are the same + if (o is string || o1 is string) { - 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; + return ((o as string).IsNullOrWhiteSpace() && (o1 as string).IsNullOrWhiteSpace()) || + (o != null && o1 != null && o.Equals(o1)); } - finally + + if (o == null || o1 == null) { - property.EnableChangeTracking(); + return false; } - } - /// - /// Used for constructing a new instance - /// - public class InitialPropertyValue - { - public InitialPropertyValue(string? culture, string? segment, bool published, object? value) + // custom comparer for enumerable + // ReSharper disable once MergeCastWithTypeCheck + if (o is IEnumerable && o1 is IEnumerable enumerable) { - Culture = culture; - Segment = segment; - Published = published; - Value = value; + return ((IEnumerable)o).Cast().UnsortedSequenceEqual(enumerable.Cast()); } - public string? Culture { get; } - public string? Segment { get; } - public bool Published { get; } - public object? Value { get; } - } + return o.Equals(o1); + }, + o => o!.GetHashCode()); - /// - /// 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 + // _pvalue contains the invariant-neutral property value + private IPropertyValue? _pvalue; - private string? _culture; - private string? _segment; + // _values contains all property values, including the invariant-neutral value + private List _values = new(); - /// - /// 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(); - } + // _vvalues contains the (indexed) variant property values + private Dictionary? _vvalues; - /// - /// 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(); - } + /// + /// Initializes a new instance of the class. + /// + public Property(IPropertyType propertyType) => PropertyType = propertyType; - /// - /// Gets or sets the edited value of the property. - /// - public object? EditedValue { get; set; } + /// + /// Initializes a new instance of the class. + /// + public Property(int id, IPropertyType propertyType) + { + Id = id; + PropertyType = propertyType; + } - /// - /// Gets or sets the published value of the property. - /// - public object? PublishedValue { get; set; } + /// + /// Returns the PropertyType, which this Property is based on + /// + [IgnoreDataMember] + public IPropertyType PropertyType { get; private set; } - /// - /// Clones the property value. - /// - public IPropertyValue Clone() - => new PropertyValue { _culture = _culture, _segment = _segment, PublishedValue = PublishedValue, EditedValue = EditedValue }; + /// + /// 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; + } + } - public object DeepClone() => Clone(); + /// + /// Returns the Alias of the PropertyType, which this Property is based on + /// + [DataMember] + public string Alias => PropertyType.Alias; - public override bool Equals(object? obj) - { - return Equals(obj as PropertyValue); - } + /// + /// Returns the Id of the PropertyType, which this Property is based on + /// + [IgnoreDataMember] + public int PropertyTypeId => PropertyType.Id; - 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); - } + /// + /// 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; - public override int GetHashCode() + /// + /// 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 (InitialPropertyValue value in values) { - var hashCode = 1885328050; - if (_culture is not null) - { - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(_culture); - } + property.FactorySetValue(value.Culture, value.Segment, value.Published, value.Value); + } - if (_segment is not null) - { - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(_segment); - } + property.ResetDirtyProperties(false); + return property; + } + finally + { + property.EnableChangeTracking(); + } + } - if (EditedValue is not null) - { - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(EditedValue); - } + /// + /// 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 (PublishedValue is not null) - { - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(PublishedValue); - } - return hashCode; - } + if (!PropertyType.SupportsVariation(culture, segment)) + { + return null; } - 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 (culture == null && segment == null) + { + return GetPropertyValue(_pvalue, published); + } - if (o == null || o1 == null) return false; + if (_vvalues == null) + { + return null; + } - // custom comparer for enumerable - // ReSharper disable once MergeCastWithTypeCheck - if (o is IEnumerable && o1 is IEnumerable enumerable) - return ((IEnumerable)o).Cast().UnsortedSequenceEqual(enumerable.Cast()); + return _vvalues.TryGetValue(new CompositeNStringNStringKey(culture, segment), out IPropertyValue? pvalue) + ? GetPropertyValue(pvalue, published) + : null; + } - return o.Equals(o1); - }, o => o!.GetHashCode()); + // 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(); - /// - /// Returns the PropertyType, which this Property is based on - /// - [IgnoreDataMember] - public IPropertyType PropertyType { get; private set; } + // 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); + } - /// - /// Gets the list of values. - /// - [DataMember] - public IReadOnlyCollection Values + // then deal with everything that varies + if (_vvalues == null) { - 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; - } + return; } - /// - /// Returns the Alias of the PropertyType, which this Property is based on - /// - [DataMember] - public string Alias => PropertyType.Alias; + // 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); - /// - /// Returns the Id of the PropertyType, which this Property is based on - /// - [IgnoreDataMember] - public int PropertyTypeId => PropertyType.Id; + foreach (IPropertyValue pvalue in pvalues) + { + PublishValue(pvalue); + } + } - /// - /// 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; + // internal - must be invoked by the content item + public void UnpublishValues(string? culture = "*", string? segment = "*") + { + culture = culture?.NullOrWhiteSpaceAsNull(); + segment = segment?.NullOrWhiteSpaceAsNull(); - /// - /// Gets the value. - /// - public object? GetValue(string? culture = null, string? segment = null, bool published = false) + // if invariant or all, and invariant-neutral is supported, publish invariant-neutral + if ((culture == null || culture == "*") && (segment == null || segment == "*") && + PropertyType.SupportsVariation(null, null)) { - // 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) - : null; + UnpublishValue(_pvalue); } - private object? GetPropertyValue(IPropertyValue? pvalue, bool published) + // then deal with everything that varies + if (_vvalues == null) { - if (pvalue == null) return null; + return; + } - return PropertyType.SupportsPublishing - ? (published ? pvalue.PublishedValue : pvalue.EditedValue) - : pvalue.EditedValue; + // 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(); - // 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 = "*") + if (!PropertyType.SupportsVariation(culture, segment)) { - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); + throw new NotSupportedException( + $"Variation \"{culture ?? ""},{segment ?? ""}\" is not supported by the property type."); + } - // 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); + (IPropertyValue? pvalue, var change) = GetPValue(culture, segment, true); - // then deal with everything that varies - if (_vvalues == null) return; + if (pvalue is not null) + { + var origValue = pvalue.EditedValue; + var setValue = ConvertAssignedValue(value); - // 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); + pvalue.EditedValue = setValue; - foreach (var pvalue in pvalues) - PublishValue(pvalue); + DetectChanges(setValue, origValue, nameof(Values), PropertyValueComparer, change); } + } - // internal - must be invoked by the content item - public void UnpublishValues(string? culture = "*", string? segment = "*") - { - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); + public object? ConvertAssignedValue(object? value) => + TryConvertAssignedValue(value, true, out var converted) ? converted : null; - // 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); + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); - // then deal with everything that varies - if (_vvalues == null) return; + var clonedEntity = (Property)clone; - // 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); + // need to manually assign since this is a readonly property + clonedEntity.PropertyType = (PropertyType)PropertyType.DeepClone(); + } - foreach (var pvalue in pvalues) - UnpublishValue(pvalue); + private object? GetPropertyValue(IPropertyValue? pvalue, bool published) + { + if (pvalue == null) + { + return null; } - private void PublishValue(IPropertyValue? pvalue) - { - if (pvalue == null) return; + return PropertyType.SupportsPublishing + ? published ? pvalue.PublishedValue : pvalue.EditedValue + : pvalue.EditedValue; + } - 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 PublishValue(IPropertyValue? pvalue) + { + if (pvalue == null) + { + return; } - private void UnpublishValue(IPropertyValue? pvalue) + if (!PropertyType.SupportsPublishing) { - if (pvalue == null) return; + throw new NotSupportedException("Property type does not support publishing."); + } - 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); + 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; } - /// - /// Sets a value. - /// - public void SetValue(object? value, string? culture = null, string? segment = null) + if (!PropertyType.SupportsPublishing) { - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); + throw new NotSupportedException("Property type does not support publishing."); + } - if (!PropertyType.SupportsVariation(culture, segment)) - throw new NotSupportedException($"Variation \"{culture??""},{segment??""}\" is not supported by the property type."); + var origValue = pvalue.PublishedValue; + pvalue.PublishedValue = ConvertAssignedValue(null); + DetectChanges(pvalue.EditedValue, origValue, nameof(Values), PropertyValueComparer, false); + } - var (pvalue, change) = GetPValue(culture, segment, true); + // 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 (pvalue is not null) + { + if (published && PropertyType.SupportsPublishing) { - var origValue = pvalue.EditedValue; - var setValue = ConvertAssignedValue(value); - - pvalue.EditedValue = setValue; - - DetectChanges(setValue, origValue, nameof(Values), PropertyValueComparer, change); + pvalue.PublishedValue = 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) - { - var (pvalue, _) = GetPValue(culture, segment, true); - - if (pvalue is not null) + else { - if (published && PropertyType.SupportsPublishing) - pvalue.PublishedValue = value; - else - pvalue.EditedValue = value; + pvalue.EditedValue = value; } } + } - private (IPropertyValue?, bool) GetPValue(bool create) + private (IPropertyValue?, bool) GetPValue(bool create) + { + var change = false; + if (_pvalue == null) { - var change = false; - if (_pvalue == null) + if (!create) { - if (!create) return (null, false); - _pvalue = new PropertyValue(); - _values.Add(_pvalue); - change = true; + return (null, false); } - return (_pvalue, change); + + _pvalue = new PropertyValue(); + _values.Add(_pvalue); + change = true; } - private (IPropertyValue?, bool) GetPValue(string? culture, string? segment, bool create) + return (_pvalue, change); + } + + private (IPropertyValue?, bool) GetPValue(string? culture, string? segment, bool create) + { + if (culture == null && segment == null) { - if (culture == null && segment == null) - return GetPValue(create); + return GetPValue(create); + } - var change = false; - if (_vvalues == null) + var change = false; + if (_vvalues == null) + { + if (!create) { - if (!create) return (null, false); - _vvalues = new Dictionary(); - change = true; + return (null, false); } - var k = new CompositeNStringNStringKey(culture, segment); - if (!_vvalues.TryGetValue(k, out var pvalue)) + + _vvalues = new Dictionary(); + change = true; + } + + var k = new CompositeNStringNStringKey(culture, segment); + if (!_vvalues.TryGetValue(k, out IPropertyValue? pvalue)) + { + if (!create) { - if (!create) return (null, false); - pvalue = _vvalues[k] = new PropertyValue(); - pvalue.Culture = culture; - pvalue.Segment = segment; - _values.Add(pvalue); - change = true; + return (null, false); } - return (pvalue, change); + + pvalue = _vvalues[k] = new PropertyValue(); + pvalue.Culture = culture; + pvalue.Segment = segment; + _values.Add(pvalue); + change = true; } - /// - public object? ConvertAssignedValue(object? value) => TryConvertAssignedValue(value, true, out var converted) ? converted : null; + return (pvalue, change); + } - /// - /// Tries to convert a value assigned to a property. - /// - /// - /// - /// - private bool TryConvertAssignedValue(object? value, bool throwOnError, out object? converted) + 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) { - var isOfExpectedType = IsOfExpectedPropertyType(value); - if (isOfExpectedType) + case ValueStorageType.Nvarchar: + case ValueStorageType.Ntext: { - converted = value; + 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 + } + + Attempt convInt = value.TryConvertTo(); + if (convInt.Success) + { + converted = convInt.Result; + return true; + } + + if (throwOnError) + { + ThrowTypeException(value, typeof(int), Alias); + } - var s = value?.ToString(); - converted = null; + return false; - switch (ValueStorageType) - { - case ValueStorageType.Nvarchar: - case ValueStorageType.Ntext: - { - converted = s; - return true; - } - - case ValueStorageType.Integer: - if (s.IsNullOrWhiteSpace()) - return true; // assume empty means null - var convInt = value.TryConvertTo(); - if (convInt.Success) - { - converted = convInt.Result; - return true; - } - - if (throwOnError) - ThrowTypeException(value, typeof(int), Alias ?? string.Empty); - return false; - - 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; - } - - if (throwOnError) - ThrowTypeException(value, typeof(decimal), Alias ?? string.Empty); - return false; - - case ValueStorageType.Date: - if (s.IsNullOrWhiteSpace()) - return true; // assume empty means null - var convDateTime = value.TryConvertTo(); - if (convDateTime.Success) - { - converted = convDateTime.Result; - return true; - } - - if (throwOnError) - ThrowTypeException(value, typeof(DateTime), Alias ?? string.Empty); - return false; - - default: - throw new NotSupportedException($"Not supported storage type \"{ValueStorageType}\"."); - } + case ValueStorageType.Decimal: + if (s.IsNullOrWhiteSpace()) + { + return true; // assume empty means null + } + + 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); + } + + return false; + + case ValueStorageType.Date: + if (s.IsNullOrWhiteSpace()) + { + return true; // assume empty means null + } + + 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}\"."); } + } - private static void ThrowTypeException(object? value, Type expected, string alias) + /// + /// 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) { - throw new InvalidOperationException($"Cannot assign value \"{value}\" of type \"{value?.GetType()}\" to property \"{alias}\" expecting type \"{expected}\"."); + return true; } + // 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) + { + 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; + /// - /// Determines whether a value is of the expected type for this property type. + /// Gets or sets the culture of the property. /// /// - /// If the value is of the expected type, it can be directly assigned to the property. - /// Otherwise, some conversion is required. + /// 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. /// - private bool IsOfExpectedPropertyType(object? value) + public string? Culture { - // null values are assumed to be ok - if (value == null) - return true; + get => _culture; + set => _culture = value.IsNullOrWhiteSpace() ? null : value!.ToLowerInvariant(); + } - // 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) - { - 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}\"."); - } + 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(); } + /// + /// 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 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); + } + + if (EditedValue is not null) + { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(EditedValue); + } + + if (PublishedValue is not null) + { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(PublishedValue); + } - //need to manually assign since this is a readonly property - clonedEntity.PropertyType = (PropertyType) PropertyType.DeepClone(); + return hashCode; } } } diff --git a/src/Umbraco.Core/Models/PropertyCollection.cs b/src/Umbraco.Core/Models/PropertyCollection.cs index 49b392ba67bb..dbb648df2947 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); - } - - /// - /// 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)); - } + /// + /// Initializes a new instance of the class. + /// + public PropertyCollection(IEnumerable properties) + : this() => + Reset(properties); - /// - /// 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)); - } + /// + /// Occurs when the collection changes. + /// + public event NotifyCollectionChangedEventHandler? CollectionChanged; - /// - /// Removes all properties. - /// - protected override void ClearItems() - { - base.ClearItems(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } + /// + /// 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) + /// + public new void Add(IProperty property) + { + // TODO: why are we locking here and not everywhere else?! + lock (_addLocker) { - 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 ... - var existing = this[key]; - - if (property.Id == 0 && existing.Id != 0) - property.Id = existing.Id; + // transfer id and values if ... + IProperty existing = this[key]; - if (property.Values.Count == 0 && existing.Values.Count > 0) - property.Values = existing.Values.Select(x => x.Clone()).ToList(); + if (property.Id == 0 && existing.Id != 0) + { + property.Id = existing.Id; + } - // replace existing with property and return, - // SetItem invokes OnCollectionChanged (but not OnAdd) - SetItem(IndexOfKey(key), property); - return; + if (property.Values.Count == 0 && existing.Values.Count > 0) + { + property.Values = existing.Values.Select(x => x.Clone()).ToList(); } - } - //collection events will be raised in InsertItem with Add - base.Add(property); + // 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); + } + } + + 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; } - /// - /// Gets the index for a specified property alias. - /// - private int IndexOfKey(string key) + foreach (IPropertyType propertyType in propertyTypes) { - for (var i = 0; i < Count; i++) - { - if (this[i].Alias?.InvariantEquals(key) ?? false) - return i; - } - return -1; + Add(new Property(propertyType)); } + } - protected override string GetKeyForItem(IProperty item) + /// + public void EnsureCleanPropertyTypes(IEnumerable propertyTypes) + { + if (propertyTypes == null) { - return item.Alias!; + return; } - /// - /// Gets the property with the specified PropertyType. - /// - internal IProperty? this[IPropertyType propertyType] + 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) { - get + if (alias is not null) { - return this.FirstOrDefault(x => x.Alias.InvariantEquals(propertyType.Alias)); + Remove(alias); } } - public bool TryGetValue(string propertyTypeAlias, [MaybeNullWhen(false)] out IProperty property) + foreach (IPropertyType propertyType in propertyTypesA) { - property = this.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); - return property != null; + Add(new Property(propertyType)); } + } - /// - /// Occurs when the collection changes. - /// - public event NotifyCollectionChangedEventHandler? CollectionChanged; - - public void ClearCollectionChangedEvents() => CollectionChanged = null; - - protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) + /// + /// Deep clones. + /// + public object DeepClone() + { + var clone = new PropertyCollection(); + foreach (IProperty property in this) { - CollectionChanged?.Invoke(this, args); + clone.Add((Property)property.DeepClone()); } + return clone; + } - /// - public void EnsurePropertyTypes(IEnumerable propertyTypes) - { - if (propertyTypes == null) - return; - - foreach (var propertyType in propertyTypes) - Add(new Property(propertyType)); - } + /// + /// 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(); - /// - public void EnsureCleanPropertyTypes(IEnumerable propertyTypes) + // collection events will be raised in each of these calls + foreach (IProperty property in properties) { - if (propertyTypes == null) - return; - - var propertyTypesA = propertyTypes.ToArray(); + Add(property); + } + } - 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); - } + /// + /// 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)); + } - foreach (var propertyType in propertyTypesA) - Add(new Property(propertyType)); - } + protected override string GetKeyForItem(IProperty item) => item.Alias; - /// - /// Deep clones. - /// - public object DeepClone() + /// + /// Gets the index for a specified property alias. + /// + private int IndexOfKey(string key) + { + for (var i = 0; i < Count; i++) { - var clone = new PropertyCollection(); - foreach (var property in this) - clone.Add((Property) property.DeepClone()); - return clone; + 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 17e66032841b..034770cdfc8d 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 { + [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)) + { + } + + public PropertyGroup(PropertyTypeCollection propertyTypeCollection) + { + PropertyTypes = propertyTypeCollection; + _alias = string.Empty; + } + /// - /// Represents a group of property types. + /// Gets or sets the type of the group. /// - [Serializable] - [DataContract(IsReference = true)] - [DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] - public class PropertyGroup : EntityBase, IEquatable + /// + /// The type. + /// + [DataMember] + public PropertyGroupType Type { - [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(bool isPublishing) - : this(new PropertyTypeCollection(isPublishing)) - { - } + get => _type; + set => SetPropertyValueAndDetectChanges(value, ref _type, nameof(Type)); + } - public PropertyGroup(PropertyTypeCollection propertyTypeCollection) - { - PropertyTypes = propertyTypeCollection; - _alias = string.Empty; - } + /// + /// Gets or sets the name of the group. + /// + /// + /// The name. + /// + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } - private void PropertyTypesChanged(object? sender, NotifyCollectionChangedEventArgs e) + /// + /// Gets or sets the alias of the group. + /// + /// + /// The alias. + /// + [DataMember] + public string Alias + { + get => _alias; + set { - OnPropertyChanged(nameof(PropertyTypes)); - } + // 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); - /// - /// Gets or sets the type of the group. - /// - /// - /// The type. - /// - [DataMember] - public PropertyGroupType Type - { - get => _type; - set => SetPropertyValueAndDetectChanges(value, ref _type, nameof(Type)); + SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); } + } - /// - /// 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 sort order of the group. + /// + /// + /// The sort order. + /// + [DataMember] + public int SortOrder + { + get => _sortOrder; + set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); + } - /// - /// Gets or sets the alias of the group. - /// - /// - /// The alias. - /// - [DataMember] - public string Alias + /// + /// 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 { - 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; - } + OnPropertyChanged(nameof(PropertyTypes)); + _propertyTypes.CollectionChanged += PropertyTypesChanged; } } + } - public bool Equals(PropertyGroup? other) => - base.Equals(other) || (other != null && Type == other.Type && Alias == other.Alias); + 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(); + public override int GetHashCode() => (base.GetHashCode(), Type, Alias).GetHashCode(); - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); - var clonedEntity = (PropertyGroup)clone; - clonedEntity.Collection = null; + 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 - } + 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 f248b12811f1..5e4479ec378a 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); - - /// - /// 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(); - - // Collection events will be raised in each of these calls - foreach (var group in groups) - Add(group); - } - - protected override void SetItem(int index, PropertyGroup item) - { - var oldItem = index >= 0 ? this[index] : item; - - base.SetItem(index, item); + } - oldItem.Collection = null; - item.Collection = this; + /// + /// Initializes a new instance of the class. + /// + /// The groups. + public PropertyGroupCollection(IEnumerable groups) => Reset(groups); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); - } + public event NotifyCollectionChangedEventHandler? CollectionChanged; - protected override void RemoveItem(int index) + public object DeepClone() + { + var clone = new PropertyGroupCollection(); + foreach (PropertyGroup group in this) { - var removed = this[index]; - - base.RemoveItem(index); - - removed.Collection = null; - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); + clone.Add((PropertyGroup)group.DeepClone()); } - protected override void InsertItem(int index, PropertyGroup item) - { - base.InsertItem(index, item); - - item.Collection = this; + return clone; + } - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + 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."); } - protected override void ClearItems() + // Note this is done to ensure existing groups can be renamed + if (item.HasIdentity && item.Id > 0) { - foreach (var item in this) + var index = IndexOfKey(item.Id); + if (index != -1) { - item.Collection = null; - } + var keyExists = Contains(item.Alias); + if (keyExists) + { + throw new ArgumentException( + $"Naming conflict: changing the alias of property group '{item.Name}' would result in duplicates."); + } - base.ClearItems(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + // Collection events will be raised in SetItem + SetItem(index, item); + return; + } } - - public new void Add(PropertyGroup item) + else { - // Ensure alias is set - if (string.IsNullOrEmpty(item.Alias)) + var index = IndexOfKey(item.Alias); + if (index != -1) { - throw new InvalidOperationException("Set an alias before adding the property group."); + // Collection events will be raised in SetItem + SetItem(index, item); + return; } + } - // 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) - throw new ArgumentException($"Naming conflict: changing the alias of property group '{item.Name}' would result in duplicates."); + // Collection events will be raised in InsertItem + base.Add(item); + } - // Collection events will be raised in SetItem - SetItem(index, item); - return; - } - } - else - { - var index = IndexOfKey(item.Alias); - if (index != -1) - { - // Collection events will be raised in SetItem - SetItem(index, item); - return; - } - } + public bool Contains(int id) => IndexOfKey(id) != -1; + + /// + /// 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(); - // Collection events will be raised in InsertItem - base.Add(item); + // Collection events will be raised in each of these calls + foreach (PropertyGroup group in groups) + { + Add(group); } + } - internal void ChangeKey(PropertyGroup item, string newKey) => ChangeItemKey(item, newKey); + protected override void SetItem(int index, PropertyGroup item) + { + PropertyGroup oldItem = index >= 0 ? this[index] : item; - public bool Contains(int id) => this.IndexOfKey(id) != -1; + base.SetItem(index, item); - public int IndexOfKey(string key) => this.FindIndex(x => x.Alias == key); + oldItem.Collection = null; + item.Collection = this; - public int IndexOfKey(int id) => this.FindIndex(x => x.Id == id); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); + } - protected override string GetKeyForItem(PropertyGroup item) => item.Alias; + protected override void RemoveItem(int index) + { + PropertyGroup removed = this[index]; - public event NotifyCollectionChangedEventHandler? CollectionChanged; + base.RemoveItem(index); - /// - /// Clears all event handlers - /// - public void ClearCollectionChangedEvents() => CollectionChanged = null; + removed.Collection = null; - protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) => CollectionChanged?.Invoke(this, args); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); + } - public object DeepClone() - { - var clone = new PropertyGroupCollection(); - foreach (var group in this) - { - clone.Add((PropertyGroup)group.DeepClone()); - } + protected override void InsertItem(int index, PropertyGroup item) + { + base.InsertItem(index, item); + + item.Collection = this; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + } - return clone; + 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 bb12e1bc1bd6..95f3bce75bc2 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 = '/'; + private const char AliasSeparator = '/'; - internal static string? GetLocalAlias(string alias) - { - var lastIndex = alias?.LastIndexOf(AliasSeparator) ?? -1; - if (lastIndex != -1) - { - return alias?.Substring(lastIndex + 1); - } + /// + /// Gets the local alias. + /// + /// The property group. + /// + /// The local alias. + /// + public static string? GetLocalAlias(this PropertyGroup propertyGroup) => GetLocalAlias(propertyGroup.Alias); - return alias; + internal static string? GetLocalAlias(string alias) + { + var lastIndex = alias?.LastIndexOf(AliasSeparator) ?? -1; + if (lastIndex != -1) + { + 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)) + { + propertyGroup.Alias = localAlias; + } + else { - var parentAlias = propertyGroup.GetParentAlias(); - 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); + /// + /// 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) + /// + /// 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 { - var localAlias = propertyGroup.GetLocalAlias(); - if (string.IsNullOrEmpty(parentAlias)) - { - propertyGroup.Alias = localAlias!; - } - else - { - propertyGroup.Alias = parentAlias + AliasSeparator + localAlias; - } + propertyGroup.Alias = parentAlias + AliasSeparator + localAlias; } } } diff --git a/src/Umbraco.Core/Models/PropertyGroupType.cs b/src/Umbraco.Core/Models/PropertyGroupType.cs index 03bcbc08f098..9111bf9bb4c7 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 7bd3a49baf0c..9ad98d66c0ac 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.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) + { + throw new ArgumentNullException(nameof(property)); + } + + IDataEditor? editor = propertyEditors[property.PropertyType?.PropertyEditorAlias]; + TagsPropertyEditorAttribute? tagAttribute = editor?.GetTagAttribute(); + if (tagAttribute == null) { - if (property == null) throw new ArgumentNullException(nameof(property)); + return null; + } - 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; + TagConfiguration? configuration = ConfigurationEditor.ConfigurationAs(configurationObject); - 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; + } - if (configuration?.Delimiter == default && configuration?.Delimiter is not null) - configuration.Delimiter = tagAttribute.Delimiter; + return configuration; + } - 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) + { + 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) + TagConfiguration? configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); + if (configuration == null) { - if (property == null) throw new ArgumentNullException(nameof(property)); + throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); + } - 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); + } - 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)); } - // 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) + TagConfiguration? configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); + if (configuration == null) { - // set the property value - var trimmedTags = tags.Select(x => x.Trim()).ToArray(); + throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); + } - if (merge) - { - var currentTags = property.GetTagsValue(storageType, serializer, delimiter); + property.RemoveTags(tags, configuration.StorageType, serializer, configuration.Delimiter, culture); + } - 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; - } - } + // 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)); } - /// - /// 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) + TagConfiguration? configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); + if (configuration == null) { - if (property == null) throw new ArgumentNullException(nameof(property)); + throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); + } - 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); + } + + /// + /// 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)); + } - property.RemoveTags(tags, configuration.StorageType, serializer, configuration.Delimiter, culture); + if (tagConfiguration == null) + { + throw new ArgumentNullException(nameof(tagConfiguration)); } - // 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) + 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) { - // already empty = nothing to do - var value = property.GetValue(culture)?.ToString(); - if (string.IsNullOrWhiteSpace(value)) return; + 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."); + switch (storageType) + { + case TagsStorageType.Csv: + property.SetValue( + string.Join(delimiter.ToString(), trimmedTags).NullOrWhiteSpaceAsNull(), + culture); // csv string + break; - return property.GetTagsValue(configuration.StorageType, serializer, configuration.Delimiter, culture); + case TagsStorageType.Json: + var updatedValue = trimmedTags.Length == 0 ? null : serializer.Serialize(trimmedTags); + property.SetValue(updatedValue, culture); // json array + break; + } } + } - private static IEnumerable GetTagsValue(this IProperty property, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture = null) + // 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)) { - if (property == null) throw new ArgumentNullException(nameof(property)); + return; + } - var value = property.GetValue(culture)?.ToString(); - if (string.IsNullOrWhiteSpace(value)) return Enumerable.Empty(); + // 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; + } + } - switch (storageType) - { - case TagsStorageType.Csv: - return value.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()); + private static IEnumerable GetTagsValue(this IProperty property, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture = null) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } - 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."); - } + var value = property.GetValue(culture)?.ToString(); + if (string.IsNullOrWhiteSpace(value)) + { + return Enumerable.Empty(); } - /// - /// 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) + switch (storageType) { - if (property == null) throw new ArgumentNullException(nameof(property)); - if (tagConfiguration == null) throw new ArgumentNullException(nameof(tagConfiguration)); + case TagsStorageType.Csv: + return value.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()); - var storageType = tagConfiguration.StorageType; - var delimiter = tagConfiguration.Delimiter; + case TagsStorageType.Json: + try + { + return serializer.Deserialize(value)?.Select(x => x.Trim()) ?? Enumerable.Empty(); + } + catch (Exception) + { + // cannot parse, malformed + return Enumerable.Empty(); + } - SetTagsValue(property, value, storageType, serializer, delimiter, culture); + 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) + // 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) { - if (value == null) value = Enumerable.Empty(); + 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; - } + // 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; + // 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 - { - 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()); - } - 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()); + } - default: - throw new ArgumentOutOfRangeException(nameof(storageType)); - } + break; + + default: + throw new ArgumentOutOfRangeException(nameof(storageType)); } } } diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 3acbad2720bc..0699ecbc0db2 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) - { - _alias = SanitizeAlias(propertyTypeAlias); - } + _shortStringHelper = shortStringHelper; - /// - /// Initializes a new instance of the class. - /// - public PropertyType(IShortStringHelper shortStringHelper,string propertyEditorAlias, ValueStorageType valueStorageType) - : this(shortStringHelper, propertyEditorAlias, valueStorageType, false) + if (dataType.HasIdentity) { + _dataTypeId = dataType.Id; } - /// - /// Initializes a new instance of the class. - /// - public PropertyType(IShortStringHelper shortStringHelper,string propertyEditorAlias, ValueStorageType valueStorageType, string propertyTypeAlias) - : this(shortStringHelper, propertyEditorAlias, valueStorageType, false, propertyTypeAlias) - { - } + _propertyEditorAlias = dataType.EditorAlias; + _valueStorageType = dataType.DatabaseType; + _variations = ContentVariation.Nothing; + _alias = string.Empty; + _name = string.Empty; + } - /// - /// 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, IDataType dataType, string propertyTypeAlias) + : this(shortStringHelper, dataType) => + _alias = SanitizeAlias(propertyTypeAlias); - /// - /// 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 Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); - } + /// + /// Initializes a new instance of the class. + /// + public PropertyType(IShortStringHelper shortStringHelper, string propertyEditorAlias, ValueStorageType valueStorageType) + : this(shortStringHelper, propertyEditorAlias, valueStorageType, false) + { + } - /// - [DataMember] - public virtual string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(SanitizeAlias(value), ref _alias!, nameof(Alias)); - } + /// + /// 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? Description - { - get => _description; - set => SetPropertyValueAndDetectChanges(value, ref _description, nameof(Description)); - } + /// + /// 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 int DataTypeId - { - get => _dataTypeId; - set => SetPropertyValueAndDetectChanges(value, ref _dataTypeId, nameof(DataTypeId)); - } + /// + /// 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; } + + /// + public bool Equals(PropertyType? other) => + other != null && (base.Equals(other) || (Alias?.InvariantEquals(other.Alias) ?? false)); + + /// + [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 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 + /// + [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)); + SetPropertyValueAndDetectChanges(value, ref _valueStorageType, nameof(ValueStorageType)); } + } + /// + [DataMember] + [DoNotClone] + public Lazy? PropertyGroupId + { + get => _propertyGroupId; + set => SetPropertyValueAndDetectChanges(value, ref _propertyGroupId, nameof(PropertyGroupId)); + } - /// - [DataMember] - public string? MandatoryMessage - { - get => _mandatoryMessage; - set => SetPropertyValueAndDetectChanges(value, ref _mandatoryMessage, nameof(MandatoryMessage)); - } + /// + [DataMember] + public bool Mandatory + { + get => _mandatory; + set => SetPropertyValueAndDetectChanges(value, ref _mandatory, nameof(Mandatory)); + } - /// - [DataMember] - public bool LabelOnTop - { - get => _labelOnTop; - set => SetPropertyValueAndDetectChanges(value, ref _labelOnTop, nameof(LabelOnTop)); - } + /// + [DataMember] + public string? MandatoryMessage + { + get => _mandatoryMessage; + set => SetPropertyValueAndDetectChanges(value, ref _mandatoryMessage, nameof(MandatoryMessage)); + } - /// - [DataMember] - public int SortOrder - { - get => _sortOrder; - set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); - } + /// + [DataMember] + public bool LabelOnTop + { + get => _labelOnTop; + set => SetPropertyValueAndDetectChanges(value, ref _labelOnTop, nameof(LabelOnTop)); + } - /// - [DataMember] - public string? ValidationRegExp - { - get => _validationRegExp; - set => SetPropertyValueAndDetectChanges(value, ref _validationRegExp, nameof(ValidationRegExp)); - } + /// + [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)); - } + /// + /// 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 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 SupportsVariation(string? culture, string? segment, bool 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 + // 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); - return value.StartsWith(Constants.PropertyEditors.InternalGenericPropertiesPrefix) - ? value - : value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase); - } + /// + public override int GetHashCode() + { + // Get hash code for the Name field if it is not null. + var baseHash = base.GetHashCode(); - /// - public bool Equals(PropertyType? other) - { - return other != null && (base.Equals(other) || (Alias?.InvariantEquals(other.Alias) ?? false)); - } + // Get hash code for the Alias field. + var hashAlias = Alias?.ToLowerInvariant().GetHashCode(); - /// - public override int GetHashCode() - { - //Get hash code for the Name field if it is not null. - int baseHash = base.GetHashCode(); + // Calculate the hash code for the product. + return baseHash ^ hashAlias ?? baseHash; + } - //Get hash code for the Alias field. - int? hashAlias = Alias?.ToLowerInvariant().GetHashCode(); + /// + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); - //Calculate the hash code for the product. - return baseHash ^ hashAlias ?? baseHash; - } + var clonedEntity = (PropertyType)clone; - /// - protected override void PerformDeepClone(object clone) + // need to manually assign the Lazy value as it will not be automatically mapped + if (PropertyGroupId != null) { - 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); - } + 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 96133f667703..49c83b4c9da1 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 - public PropertyTypeCollection(bool supportsPublishing, IEnumerable properties) - : this(supportsPublishing) - { - Reset(properties); - } +/// +/// Represents a collection of objects. +/// +[Serializable] +[DataContract] - public bool SupportsPublishing { get; } +// 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; - // This baseclass calling is needed, else compiler will complain about nullability + public PropertyTypeCollection(bool supportsPublishing, IEnumerable properties) + : this(supportsPublishing) => + Reset(properties); - /// - public bool IsReadOnly => ((ICollection)this).IsReadOnly; + public event NotifyCollectionChangedEventHandler? CollectionChanged; - /// - /// 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 bool SupportsPublishing { get; } - //collection events will be raised in each of these calls - foreach (var property in properties) - Add(property); - } + // This baseclass calling is needed, else compiler will complain about nullability - 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; - } + /// + public bool IsReadOnly => ((ICollection)this).IsReadOnly; - protected override void RemoveItem(int index) - { - var removed = this[index]; - base.RemoveItem(index); - removed.PropertyChanged -= Item_PropertyChanged; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); - } + // '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; - protected override void InsertItem(int index, IPropertyType item) + // TODO: this is not pretty and should be refactored + var key = GetKeyForItem(item); + if (key != null) { - item.SupportsPublishing = SupportsPublishing; - base.InsertItem(index, item); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); - item.PropertyChanged += Item_PropertyChanged; + var exists = Contains(key); + if (exists) + { + // collection events will be raised in SetItem + SetItem(IndexOfKey(key), item); + return; + } } - protected override void ClearItems() + // check if the item's sort order is already in use + if (this.Any(x => x.SortOrder == item.SortOrder)) { - base.ClearItems(); - foreach (var item in this) - item.PropertyChanged -= Item_PropertyChanged; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + // make it the next iteration + item.SortOrder = this.Max(x => x.SortOrder) + 1; } - // '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; + // collection events will be raised in InsertItem + base.Add(item); + } - // TODO: this is not pretty and should be refactored + public object DeepClone() + { + var clone = new PropertyTypeCollection(SupportsPublishing); + foreach (IPropertyType propertyType in this) + { + clone.Add((IPropertyType)propertyType.DeepClone()); + } - var key = GetKeyForItem(item); - if (key != null) - { - var exists = Contains(key); - if (exists) - { - //collection events will be raised in SetItem - SetItem(IndexOfKey(key), item); - return; - } - } + return clone; + } - //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; - } + /// + /// 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); - //collection events will be raised in InsertItem - base.Add(item); - } + /// + /// 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(); - /// - /// Occurs when a property changes on a IPropertyType that exists in this collection - /// - /// - /// - private void Item_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + // collection events will be raised in each of these calls + foreach (IPropertyType property in properties) { - var propType = (IPropertyType?)sender; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, propType, propType)); + Add(property); } + } - /// - /// 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) - { - return this.Any(x => x.Alias == propertyAlias); - } + 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; + } - public bool RemoveItem(string propertyTypeAlias) - { - var key = IndexOfKey(propertyTypeAlias); - if (key != -1) RemoveItem(key); - return key != -1; - } + protected override void RemoveItem(int index) + { + IPropertyType removed = this[index]; + base.RemoveItem(index); + removed.PropertyChanged -= Item_PropertyChanged; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); + } - public int IndexOfKey(string key) - { - for (var i = 0; i < Count; i++) - if (this[i].Alias == key) - return i; - return -1; - } + 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 string GetKeyForItem(IPropertyType item) + protected override void ClearItems() + { + base.ClearItems(); + foreach (IPropertyType item in this) { - return item.Alias!; + item.PropertyChanged -= Item_PropertyChanged; } - public event NotifyCollectionChangedEventHandler? CollectionChanged; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } - /// - /// Clears all event handlers - /// - public void ClearCollectionChangedEvents() => CollectionChanged = null; - protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) + /// + /// 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) { - CollectionChanged?.Invoke(this, args); + RemoveItem(key); } - public object DeepClone() + return key != -1; + } + + public int IndexOfKey(string key) + { + for (var i = 0; i < Count; i++) { - var clone = new PropertyTypeCollection(SupportsPublishing); - foreach (var propertyType in this) - clone.Add((IPropertyType) propertyType.DeepClone()); - return clone; + 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 00e05442d8a2..8789ef505279 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 EventClearingObservableCollection _ruleCollection; - private int _protectedNodeId; - private int _noAccessNodeId; - private int _loginNodeId; - private readonly List _removedRules = new List(); + 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) + 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) - { - var item = e.OldItems?.Cast().First(); - - if (item is not null) - { - if (_removedRules.Contains(item.Key) == false) - { - _removedRules.Add(item.Key); - } - } - } + foreach (PublicAccessRule rule in _ruleCollection) + { + rule.AccessEntryId = Key; } + } - public IEnumerable RemovedRules => _removedRules; + public PublicAccessEntry(Guid id, int protectedNodeId, int loginNodeId, int noAccessNodeId, IEnumerable ruleCollection) + { + Key = id; + Id = Key.GetHashCode(); - public IEnumerable Rules => _ruleCollection; + LoginNodeId = loginNodeId; + NoAccessNodeId = noAccessNodeId; + _protectedNodeId = protectedNodeId; - public PublicAccessRule AddRule(string ruleValue, string ruleType) - { - var rule = new PublicAccessRule - { - AccessEntryId = Key, - RuleValue = ruleValue, - RuleType = ruleType - }; - _ruleCollection.Add(rule); - return rule; - } + _ruleCollection = new EventClearingObservableCollection(ruleCollection); + _ruleCollection.CollectionChanged += RuleCollection_CollectionChanged; - public void RemoveRule(PublicAccessRule rule) + foreach (PublicAccessRule rule in _ruleCollection) { - _ruleCollection.Remove(rule); + rule.AccessEntryId = Key; } + } - public void ClearRules() - { - _ruleCollection.Clear(); - } + public IEnumerable RemovedRules => _removedRules; + public IEnumerable Rules => _ruleCollection; - internal void ClearRemovedRules() - { - _removedRules.Clear(); - } + [DataMember] + public int LoginNodeId + { + get => _loginNodeId; + set => SetPropertyValueAndDetectChanges(value, ref _loginNodeId, nameof(LoginNodeId)); + } - [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 NoAccessNodeId - { - get => _noAccessNodeId; - set => SetPropertyValueAndDetectChanges(value, ref _noAccessNodeId, nameof(NoAccessNodeId)); - } + [DataMember] + public int ProtectedNodeId + { + get => _protectedNodeId; + set => SetPropertyValueAndDetectChanges(value, ref _protectedNodeId, nameof(ProtectedNodeId)); + } - [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; + } - public override void ResetDirtyProperties(bool rememberDirty) + 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) { - _removedRules.Clear(); - base.ResetDirtyProperties(rememberDirty); - foreach (var publicAccessRule in _ruleCollection) + PublicAccessRule? item = e.OldItems?.Cast().First(); + + if (item is not null) { - publicAccessRule.ResetDirtyProperties(rememberDirty); + if (_removedRules.Contains(item.Key) == false) + { + _removedRules.Add(item.Key); + } } } + } + + public void RemoveRule(PublicAccessRule rule) => _ruleCollection.Remove(rule); - protected override void PerformDeepClone(object clone) + public void ClearRules() => _ruleCollection.Clear(); + + public override void ResetDirtyProperties(bool rememberDirty) + { + _removedRules.Clear(); + base.ResetDirtyProperties(rememberDirty); + foreach (PublicAccessRule publicAccessRule in _ruleCollection) { - base.PerformDeepClone(clone); + publicAccessRule.ResetDirtyProperties(rememberDirty); + } + } - var cloneEntity = (PublicAccessEntry)clone; + internal void ClearRemovedRules() => _removedRules.Clear(); - if (cloneEntity._ruleCollection != null) - { - cloneEntity._ruleCollection.ClearCollectionChangedEvents(); //clear this event handler if any - cloneEntity._ruleCollection.CollectionChanged += cloneEntity._ruleCollection_CollectionChanged; //re-assign correct event handler - } + 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 } } } diff --git a/src/Umbraco.Core/Models/PublicAccessRule.cs b/src/Umbraco.Core/Models/PublicAccessRule.cs index 790d8b6a1be7..f8af1a6d980e 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 -{ - [Serializable] - [DataContract(IsReference = true)] - public class PublicAccessRule : EntityBase - { - private string? _ruleValue; - private string? _ruleType; - - public PublicAccessRule(Guid id, Guid accessEntryId) - { - AccessEntryId = accessEntryId; - Key = id; - Id = Key.GetHashCode(); - } +namespace Umbraco.Cms.Core.Models; - public PublicAccessRule() - { - } +[Serializable] +[DataContract(IsReference = true)] +public class PublicAccessRule : EntityBase +{ + private string? _ruleType; + private string? _ruleValue; - public Guid AccessEntryId { get; set; } + public PublicAccessRule(Guid id, Guid accessEntryId) + { + AccessEntryId = accessEntryId; + Key = id; + Id = Key.GetHashCode(); + } - public string? RuleValue - { - get => _ruleValue; - set => SetPropertyValueAndDetectChanges(value, ref _ruleValue, nameof(RuleValue)); - } + public PublicAccessRule() + { + } - 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 1aaa0d98142e..2c665f1710c0 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 3fb18fad2dcf..6d8fe9e54790 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 - { - private readonly IRequestCache _requestCache; - private const string ContextKey = "Umbraco.Web.Models.PublishedContent.DefaultVariationContextAccessor"; + public HttpContextVariationContextAccessor(IRequestCache requestCache) => _requestCache = requestCache; - /// - /// 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); - } + /// + public VariationContext? VariationContext + { + 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 d974041d3b4b..2be963843856 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 +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Implements a hybrid . +/// +public class HybridVariationContextAccessor : HybridAccessorBase, IVariationContextAccessor { + public HybridVariationContextAccessor(IRequestCache requestCache) + : base(requestCache) + { + } + /// - /// Implements a hybrid . + /// Gets or sets the object. /// - public class HybridVariationContextAccessor : HybridAccessorBase, IVariationContextAccessor + public VariationContext? VariationContext { - public HybridVariationContextAccessor(IRequestCache requestCache) - : base(requestCache) - { } - - /// - /// Gets or sets the object. - /// - public VariationContext? VariationContext - { - get => Value; - set => Value = value; - } + get => Value; + set => Value = value; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IAutoPublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IAutoPublishedModelFactory.cs index 2838297a8e20..37ca5b3733c2 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 +{ /// - /// Provides a live published model creation service. + /// Gets an object that can be used to synchronize access to the factory. /// - public interface IAutoPublishedModelFactory : IPublishedModelFactory - { - /// - /// Gets an object that can be used to synchronize access to the factory. - /// - object SyncRoot { get; } + object SyncRoot { get; } - /// - /// Tells the factory that it should build a new generation of models - /// - void Reset(); + /// + /// If the live model factory + /// + bool Enabled { get; } - /// - /// 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 eb5233993687..01b57f38f8cf 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 + + /// + /// 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. + /// + /// + /// 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; } - /// /// - /// Represents a published content item. + /// Gets available culture infos. /// /// - /// Can be a published document, media or member. + /// + /// 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? /// - public interface IPublishedContent : IPublishedElement - { - #region Content - - // 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 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. - /// - /// - /// 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 - } + IReadOnlyDictionary Cultures { get; } + + /// + /// Gets the type of the content item (document, media...). + /// + PublishedItemType ItemType { get; } + + /// + /// Gets the parent of the content item. + /// + /// The parent of root content is null. + IPublishedContent? Parent { 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); + + /// + /// 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; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs index bd3f77152d85..5ce8bef875b8 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 b1a1740b31dd..09e9a00389a3 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 published content types. + /// Creates a published content 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); + /// An content type. + /// A published content type corresponding to the item type and content type. + IPublishedContentType CreateContentType(IContentTypeComposition contentType); - /// - /// 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. + /// 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 767d3eadc01c..a198064137dc 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 + + /// + /// Gets the content type. + /// + IPublishedContentType ContentType { get; } + + #endregion + + #region PublishedElement + + /// + /// Gets the unique key of the published element. + /// + Guid Key { get; } + + #endregion + + #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; } + /// - /// Represents a published element. + /// Gets a property identified by its alias. /// - public interface IPublishedElement - { - #region ContentType - - /// - /// Gets the content type. - /// - IPublishedContentType ContentType { get; } - - #endregion - - #region PublishedElement - - /// - /// Gets the unique key of the published element. - /// - Guid Key { get; } - - #endregion - - #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 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 - } + /// 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 } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedMemberCache.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedMemberCache.cs index ba8bdc43d480..cefb51241ef2 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 c34a4a6ba4cc..03485f0b6c35 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 804d0972daf0..b030f145fd2c 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; } - - /// - /// Gets the alias of the property. - /// - string Alias { get; } + 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 3ab21d15f6b2..3caaee9a3776 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 c1ecf1909a45..729f7dd6bc31 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 83c5f19c9edc..a20820d9543f 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 7c7049c02639..fe7fe2a47480 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 0de838fa0eff..4588d47967fa 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 { + private ModelType(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)); + } + + ContentTypeAlias = contentTypeAlias; + Name = "{" + ContentTypeAlias + "}"; + } + + /// + /// 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; + /// - /// Represents the CLR type of a model. + /// Gets the model type for a published element type. /// - /// - /// ModelType.For("alias") - /// typeof (IEnumerable{}).MakeGenericType(ModelType.For("alias")) - /// Model.For("alias").MakeArrayType() - /// - public class ModelType : 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) { - private ModelType(string? contentTypeAlias) + // it may be that senders forgot to send an invariant dictionary (garbage-in) + if (modelTypes is not null && !dictionaryIsInvariant) { - 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 + "}"; + modelTypes = new Dictionary(modelTypes, StringComparer.InvariantCultureIgnoreCase); } - /// - /// 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 (type is ModelType modelType) { - // 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 Type? actualType) ?? false) { - 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}\"."); + return actualType; } - if (type is ModelTypeArrayType arrayType) + 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) { - 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}\"."); + return actualType.MakeArrayType(); } - 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 InvalidOperationException( + $"Don't know how to map ModelType with content type alias \"{arrayType.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); + if (type.IsGenericType == false) + { + return type; + } - private static string MapToName(Type type, Dictionary map, bool dictionaryIsInvariant) + Type def = type.GetGenericTypeDefinition(); + if (def == null) { - // it may be that senders forgot to send an invariant dictionary (garbage-in) - if (!dictionaryIsInvariant) - map = new Dictionary(map, StringComparer.InvariantCultureIgnoreCase); + throw new PanicException($"The type {type} has not generic type definition"); + } - 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}\"."); - } + Type[] args = type.GetGenericArguments().Select(x => Map(x, modelTypes, true)).ToArray(); + return def.MakeGenericType(args); + } - 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}\"."); - } + /// + /// 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); - 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"); + /// + /// 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; + } - 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) + ">"; + if (t1 is ModelType m1 && t2 is ModelType m2) + { + return m1.ContentTypeAlias == m2.ContentTypeAlias; } - /// - /// 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 is ModelTypeArrayType a1 && t2 is ModelTypeArrayType a2) { - if (t1 == t2) - return true; + return a1.ContentTypeAlias == a2.ContentTypeAlias; + } - if (t1 is ModelType m1 && t2 is ModelType m2) - return m1.ContentTypeAlias == m2.ContentTypeAlias; + if (t1.IsGenericType == false || t2.IsGenericType == false) + { + return false; + } - if (t1 is ModelTypeArrayType a1 && t2 is ModelTypeArrayType a2) - return a1.ContentTypeAlias == a2.ContentTypeAlias; + Type[] args1 = t1.GetGenericArguments(); + Type[] args2 = t2.GetGenericArguments(); + if (args1.Length != args2.Length) + { + return false; + } - if (t1.IsGenericType == false || t2.IsGenericType == false) + for (var i = 0; i < args1.Length; i++) + { + // ReSharper disable once CheckForReferenceEqualityInstead.2 + if (Equals(args1[i], args2[i]) == false) + { return false; + } + } + + return true; + } + + /// + public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) + => Array.Empty(); - var args1 = t1.GetGenericArguments(); - var args2 = t2.GetGenericArguments(); - if (args1.Length != args2.Length) return false; + /// + public override Type[] GetInterfaces() + => Array.Empty(); + + private static string MapToName(Type type, Dictionary map, bool dictionaryIsInvariant) + { + // it may be that senders forgot to send an invariant dictionary (garbage-in) + if (!dictionaryIsInvariant) + { + map = new Dictionary(map, StringComparer.InvariantCultureIgnoreCase); + } - for (var i = 0; i < args1.Length; i++) + if (type is ModelType modelType) + { + if (map.TryGetValue(modelType.ContentTypeAlias, out var actualTypeName)) { - // ReSharper disable once CheckForReferenceEqualityInstead.2 - if (Equals(args1[i], args2[i]) == false) return false; + return actualTypeName; } - return true; + throw new InvalidOperationException( + $"Don't know how to map ModelType with content type alias \"{modelType.ContentTypeAlias}\"."); } - /// - protected override TypeAttributes GetAttributeFlagsImpl() - => TypeAttributes.Class; + if (type is ModelTypeArrayType arrayType) + { + if (map.TryGetValue(arrayType.ContentTypeAlias, out var actualTypeName)) + { + return actualTypeName + "[]"; + } - /// - public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) - => Array.Empty(); + throw new InvalidOperationException( + $"Don't know how to map ModelType with content type alias \"{arrayType.ContentTypeAlias}\"."); + } - /// - protected override ConstructorInfo? GetConstructorImpl(BindingFlags bindingAttr, Binder? binder, CallingConventions callConvention, Type[] types, ParameterModifier[]? modifiers) - => null; + if (type.IsGenericType == false) + { + return type.FullName!; + } - /// - public override Type[] GetInterfaces() - => Array.Empty(); + Type def = type.GetGenericTypeDefinition(); + if (def == null) + { + throw new PanicException($"The type {type} has not generic type definition"); + } - /// - public override Type? GetInterface(string name, bool ignoreCase) - => null; + var args = type.GetGenericArguments().Select(x => MapToName(x, map, true)).ToArray(); + var defFullName = def.FullName?[..def.FullName.IndexOf('`')]; + return defFullName + "<" + string.Join(", ", args) + ">"; + } - /// - public override EventInfo[] GetEvents(BindingFlags bindingAttr) - => Array.Empty(); + /// + protected override TypeAttributes GetAttributeFlagsImpl() + => TypeAttributes.Class; - /// - public override EventInfo? GetEvent(string name, BindingFlags bindingAttr) - => null; + /// + protected override ConstructorInfo? GetConstructorImpl( + BindingFlags bindingAttr, + Binder? binder, + CallingConventions callConvention, + Type[] types, + ParameterModifier[]? modifiers) + => null; - /// - public override Type[] GetNestedTypes(BindingFlags bindingAttr) - => Array.Empty(); + /// + public override Type? GetInterface(string name, bool ignoreCase) + => null; - /// - public override Type? GetNestedType(string name, BindingFlags bindingAttr) - => null; + /// + public override EventInfo[] GetEvents(BindingFlags bindingAttr) + => Array.Empty(); - /// - public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) - => Array.Empty(); + /// + public override EventInfo? GetEvent(string name, BindingFlags bindingAttr) + => null; - /// - protected override PropertyInfo? GetPropertyImpl(string name, BindingFlags bindingAttr, Binder? binder, Type? returnType, Type[]? types, ParameterModifier[]? modifiers) - => null; + /// + public override Type[] GetNestedTypes(BindingFlags bindingAttr) + => Array.Empty(); - /// - public override MethodInfo[] GetMethods(BindingFlags bindingAttr) - => Array.Empty(); + /// + public override Type? GetNestedType(string name, BindingFlags bindingAttr) + => null; - /// - protected override MethodInfo? GetMethodImpl(string name, BindingFlags bindingAttr, Binder? binder, CallingConventions callConvention, Type[]? types, ParameterModifier[]? modifiers) - => null; + /// + public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) + => Array.Empty(); - /// - public override FieldInfo[] GetFields(BindingFlags bindingAttr) - => Array.Empty(); + /// + public override MethodInfo[] GetMethods(BindingFlags bindingAttr) + => Array.Empty(); - /// - public override FieldInfo? GetField(string name, BindingFlags bindingAttr) - => null; + /// + public override FieldInfo[] GetFields(BindingFlags bindingAttr) + => Array.Empty(); - /// - public override MemberInfo[] GetMembers(BindingFlags bindingAttr) - => Array.Empty(); + /// + protected override PropertyInfo? GetPropertyImpl( + string name, + BindingFlags bindingAttr, + Binder? binder, + Type? returnType, + Type[]? types, + ParameterModifier[]? modifiers) + => null; - /// - public override object[] GetCustomAttributes(Type attributeType, bool inherit) - => Array.Empty(); + /// + protected override MethodInfo? GetMethodImpl( + string name, + BindingFlags bindingAttr, + Binder? binder, + CallingConventions callConvention, + Type[]? types, + ParameterModifier[]? modifiers) + => null; - /// - public override object[] GetCustomAttributes(bool inherit) - => Array.Empty(); + /// + public override FieldInfo? GetField(string name, BindingFlags bindingAttr) + => null; - /// - public override bool IsDefined(Type attributeType, bool inherit) - => false; + /// + public override MemberInfo[] GetMembers(BindingFlags bindingAttr) + => Array.Empty(); - /// - public override Type? GetElementType() - => null; + /// + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + => Array.Empty(); - /// - protected override bool HasElementTypeImpl() - => false; + /// + public override object[] GetCustomAttributes(bool inherit) + => Array.Empty(); - /// - protected override bool IsArrayImpl() - => false; + /// + public override bool IsDefined(Type attributeType, bool inherit) + => false; - /// - protected override bool IsByRefImpl() - => false; + /// + public override Type? GetElementType() + => null; - /// - protected override bool IsPointerImpl() - => false; + /// + 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 IsPrimitiveImpl() - => false; + /// + protected override bool HasElementTypeImpl() + => false; - /// - protected override bool IsCOMObjectImpl() - => false; + /// + protected override bool IsArrayImpl() + => false; - /// - 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 IsByRefImpl() + => false; - /// - public override Type UnderlyingSystemType => this; + /// + protected override bool IsPointerImpl() + => false; - /// - public override Type? BaseType => null; + /// + protected override bool IsPrimitiveImpl() + => false; - /// - public override string Name { get; } + /// + protected override bool IsCOMObjectImpl() + => false; - /// - public override Guid GUID { get; } = Guid.NewGuid(); + /// + public override Type MakeArrayType() + => new ModelTypeArrayType(this); +} - /// - public override Module Module => GetType().Module; // hackish but FullName requires something +/// +internal class ModelTypeArrayType : Type +{ + private readonly Type _elementType; - /// - public override Assembly Assembly => GetType().Assembly; // hackish but FullName requires something + public ModelTypeArrayType(ModelType type) + { + _elementType = type; + ContentTypeAlias = type.ContentTypeAlias; + Name = "{" + type.ContentTypeAlias + "}[*]"; + } - /// - public override string FullName => Name; + public string ContentTypeAlias { get; } - /// - public override string Namespace => string.Empty; + public override Type UnderlyingSystemType => this; - /// - public override string AssemblyQualifiedName => Name; + public override Type? BaseType => null; - /// - public override Type MakeArrayType() - => new ModelTypeArrayType(this); - } + public override string Name { get; } - internal class ModelTypeArrayType : Type - { - private readonly Type _elementType; + public override Guid GUID { get; } = Guid.NewGuid(); - public ModelTypeArrayType(ModelType type) - { - _elementType = type; - ContentTypeAlias = type.ContentTypeAlias; - Name = "{" + type.ContentTypeAlias + "}[*]"; - } + public override Module Module => GetType().Module; // hackish but FullName requires something - public string ContentTypeAlias { get; } + public override Assembly Assembly => GetType().Assembly; // hackish but FullName requires something - public override string ToString() - => Name; + public override string FullName => Name; - protected override TypeAttributes GetAttributeFlagsImpl() - => TypeAttributes.Class; + public override string Namespace => string.Empty; - public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) - => Array.Empty(); + public override string AssemblyQualifiedName => Name; - protected override ConstructorInfo? GetConstructorImpl(BindingFlags bindingAttr, Binder? binder, CallingConventions callConvention, Type[] types, ParameterModifier[]? modifiers) - => null; + public override string ToString() + => Name; - public override Type[] GetInterfaces() - => Array.Empty(); + public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) + => Array.Empty(); - public override Type? GetInterface(string name, bool ignoreCase) - => null; + public override Type[] GetInterfaces() + => Array.Empty(); - public override EventInfo[] GetEvents(BindingFlags bindingAttr) - => Array.Empty(); + protected override TypeAttributes GetAttributeFlagsImpl() + => TypeAttributes.Class; - public override EventInfo? GetEvent(string name, BindingFlags bindingAttr) - => null; + protected override ConstructorInfo? GetConstructorImpl( + BindingFlags bindingAttr, + Binder? binder, + CallingConventions callConvention, + Type[] types, + ParameterModifier[]? modifiers) + => null; - public override Type[] GetNestedTypes(BindingFlags bindingAttr) - => Array.Empty(); + public override Type? GetInterface(string name, bool ignoreCase) + => null; - public override Type? GetNestedType(string name, BindingFlags bindingAttr) - => null; + public override EventInfo[] GetEvents(BindingFlags bindingAttr) + => Array.Empty(); - public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) - => Array.Empty(); + public override EventInfo? GetEvent(string name, BindingFlags bindingAttr) + => null; - protected override PropertyInfo? GetPropertyImpl(string name, BindingFlags bindingAttr, Binder? binder, Type? returnType, Type[]? types, ParameterModifier[]? modifiers) - => null; + public override Type[] GetNestedTypes(BindingFlags bindingAttr) + => Array.Empty(); - public override MethodInfo[] GetMethods(BindingFlags bindingAttr) - => Array.Empty(); + public override Type? GetNestedType(string name, BindingFlags bindingAttr) + => null; - protected override MethodInfo? GetMethodImpl(string name, BindingFlags bindingAttr, Binder? binder, CallingConventions callConvention, Type[]? types, ParameterModifier[]? modifiers) - => null; + public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) + => Array.Empty(); - public override FieldInfo[] GetFields(BindingFlags bindingAttr) - => Array.Empty(); + public override MethodInfo[] GetMethods(BindingFlags bindingAttr) + => Array.Empty(); - public override FieldInfo? GetField(string name, BindingFlags bindingAttr) - => null; + public override FieldInfo[] GetFields(BindingFlags bindingAttr) + => Array.Empty(); - public override MemberInfo[] GetMembers(BindingFlags bindingAttr) - => Array.Empty(); + protected override PropertyInfo? GetPropertyImpl( + string name, + BindingFlags bindingAttr, + Binder? binder, + Type? returnType, + Type[]? types, + ParameterModifier[]? modifiers) + => null; - public override object[] GetCustomAttributes(Type attributeType, bool inherit) - => Array.Empty(); + protected override MethodInfo? GetMethodImpl( + string name, + BindingFlags bindingAttr, + Binder? binder, + CallingConventions callConvention, + Type[]? types, + ParameterModifier[]? modifiers) + => null; - public override object[] GetCustomAttributes(bool inherit) - => Array.Empty(); + public override FieldInfo? GetField(string name, BindingFlags bindingAttr) + => null; - public override bool IsDefined(Type attributeType, bool inherit) - => false; + public override MemberInfo[] GetMembers(BindingFlags bindingAttr) + => Array.Empty(); - public override Type GetElementType() - => _elementType; + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + => Array.Empty(); - protected override bool HasElementTypeImpl() - => true; + public override object[] GetCustomAttributes(bool inherit) + => Array.Empty(); - protected override bool IsArrayImpl() - => true; + public override bool IsDefined(Type attributeType, bool inherit) + => false; - protected override bool IsByRefImpl() - => false; + public override Type GetElementType() + => _elementType; - protected override bool IsPointerImpl() - => false; + 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 IsPrimitiveImpl() - => false; + protected override bool HasElementTypeImpl() + => true; - 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(); - } + protected override bool IsArrayImpl() + => true; - public override Type UnderlyingSystemType => this; - public override Type? BaseType => null; + protected override bool IsByRefImpl() + => false; - 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; + protected override bool IsPointerImpl() + => false; - public override int GetArrayRank() - => 1; - } + 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 93b6948edc69..5eefd1e12b60 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 a08a20658dac..1dd2fef1241b 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 60d3cd4a02cd..077b420735b2 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 @@ public abstract class PublishedContentBase : IPublishedContent { 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 @@ protected PublishedContentBase(IVariationContextAccessor? variationContextAccess /// public abstract bool IsPublished(string? culture = null); - #endregion - - #region Tree - /// public abstract IPublishedContent? Parent { get; } @@ -93,16 +74,10 @@ protected PublishedContentBase(IVariationContextAccessor? variationContextAccess /// 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 f1d348d2ffe1..2b123a33a948 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; + throw new ArgumentNullException(nameof(publishedModelFactory)); + } - // get model - // if factory returns nothing, throw - var model = publishedModelFactory.CreateModel(content); - if (model == null) - throw new InvalidOperationException("Factory returned null."); + if (content == null) + { + return 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."); + // get model + // if factory returns nothing, throw + IPublishedElement model = publishedModelFactory.CreateModel(content); + if (model == null) + { + throw new InvalidOperationException("Factory returned null."); + } - return publishedContent; + // 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 249c2cb465a0..cfa648594e70 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 592c2eff5e55..bd5e7af0a490 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 @@ public PublishedContentType(IContentTypeComposition contentType, IPublishedConte .ToList(); if (ItemType == PublishedItemType.Member) + { EnsureMemberProperties(propertyTypes, factory); + } _propertyTypes = propertyTypes.ToArray(); @@ -46,9 +45,12 @@ public PublishedContentType(IContentTypeComposition contentType, IPublishedConte 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 @@ public PublishedContentType(Guid key, int id, string alias, PublishedItemType it 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 @@ private void EnsureMemberProperties(List propertyTypes, { 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 @@ private void EnsureMemberProperties(List propertyTypes, /// 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 23adf358ca0f..957246ccfe35 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) }; + private static readonly Type[] ConvertingTypes = { typeof(int) }; - public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) - { - return ConvertingTypes.Any(x => x.IsAssignableFrom(destinationType)) - || (destinationType is not null && CanConvertFrom(context, destinationType)); - } + 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) + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + { + if (!(value is IPublishedContent publishedContent)) { - if (!(value is IPublishedContent publishedContent)) - return null; - - return typeof(int).IsAssignableFrom(destinationType) - ? publishedContent.Id - : base.ConvertTo(context, culture, value, destinationType); + return null; } + + 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 5a43295981e5..f2b1b9bbcadd 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); - } + _publishedModelFactory = publishedModelFactory; + _propertyValueConverters = propertyValueConverters; + _dataTypeService = dataTypeService; + } - /// - public IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, IPropertyType propertyType) + /// + 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) { - return new PublishedPropertyType(contentType, propertyType, _propertyValueConverters, _publishedModelFactory, this); - } + if (_publishedDataTypes == null) + { + IEnumerable dataTypes = _dataTypeService.GetAll(); + _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); + } - /// - public IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) - { - return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, true, variations, _propertyValueConverters, _publishedModelFactory, this); + publishedDataTypes = _publishedDataTypes; } - /// - public IPublishedPropertyType CreateCorePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) + if (publishedDataTypes is null || !publishedDataTypes.TryGetValue(id, out PublishedDataType? dataType)) { - return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, false, variations, _propertyValueConverters, _publishedModelFactory, this); + throw new ArgumentException($"Could not find a datatype with identifier {id}.", nameof(id)); } - /// - /// 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); - } + return dataType; + } - /// - public PublishedDataType GetDataType(int id) + /// + public void NotifyDataTypeChanges(int[] ids) + { + lock (_publishedDataTypesLocker) { - Dictionary? publishedDataTypes; - lock (_publishedDataTypesLocker) + if (_publishedDataTypes == null) { - if (_publishedDataTypes == null) - { - var dataTypes = _dataTypeService.GetAll(); - _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); - } - - publishedDataTypes = _publishedDataTypes; + IEnumerable dataTypes = _dataTypeService.GetAll(); + _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); } - - 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) + else { - if (_publishedDataTypes == null) + foreach (var id in ids) { - var dataTypes = _dataTypeService.GetAll(); - _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); + _publishedDataTypes.Remove(id); } - else - { - foreach (var id in ids) - _publishedDataTypes.Remove(id); - var dataTypes = _dataTypeService.GetAll(ids); - foreach (var dataType in dataTypes) - _publishedDataTypes[dataType.Id] = CreatePublishedDataType(dataType); + IEnumerable dataTypes = _dataTypeService.GetAll(ids); + foreach (IDataType 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)); } + + /// + /// 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 9d16de743d8a..b5e9a94e1349 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 + _content = content; + _publishedValueFallback = publishedValueFallback; + } - /// - public virtual int Id => _content.Id; + /// + public virtual IPublishedContentType ContentType => _content.ContentType; - /// - public virtual string? Name => _content.Name; + /// + public Guid Key => _content.Key; - /// - public virtual string? UrlSegment => _content.UrlSegment; + #region PublishedContent - /// - public virtual int SortOrder => _content.SortOrder; + /// + public virtual int Id => _content.Id; - /// - public virtual int Level => _content.Level; + #endregion - /// - public virtual string Path => _content.Path; + /// + /// Gets the wrapped content. + /// + /// The wrapped content, that was passed as an argument to the constructor. + public IPublishedContent Unwrap() => _content; - /// - public virtual int? TemplateId => _content.TemplateId; + /// + public virtual string? Name => _content.Name; - /// - public virtual int CreatorId => _content.CreatorId; + /// + public virtual string? UrlSegment => _content.UrlSegment; - /// - public virtual DateTime CreateDate => _content.CreateDate; + /// + public virtual int SortOrder => _content.SortOrder; - /// - public virtual int WriterId => _content.WriterId; + /// + public virtual int Level => _content.Level; - /// - public virtual DateTime UpdateDate => _content.UpdateDate; + /// + public virtual string Path => _content.Path; - /// - public IReadOnlyDictionary Cultures => _content.Cultures; + /// + public virtual int? TemplateId => _content.TemplateId; - /// - public virtual PublishedItemType ItemType => _content.ItemType; + /// + public virtual int CreatorId => _content.CreatorId; - /// - public virtual bool IsDraft(string? culture = null) => _content.IsDraft(culture); + /// + public virtual DateTime CreateDate => _content.CreateDate; - /// - public virtual bool IsPublished(string? culture = null) => _content.IsPublished(culture); + /// + public virtual int WriterId => _content.WriterId; - #endregion + /// + public virtual DateTime UpdateDate => _content.UpdateDate; - #region Tree + /// + public IReadOnlyDictionary Cultures => _content.Cultures; - /// - public virtual IPublishedContent? Parent => _content.Parent; + /// + public virtual PublishedItemType ItemType => _content.ItemType; - /// - public virtual IEnumerable? Children => _content.Children; + /// + public virtual IPublishedContent? Parent => _content.Parent; - /// - public virtual IEnumerable? ChildrenForAllCultures => _content.ChildrenForAllCultures; + /// + public virtual bool IsDraft(string? culture = null) => _content.IsDraft(culture); - #endregion + /// + public virtual bool IsPublished(string? culture = null) => _content.IsPublished(culture); - #region Properties + /// + public virtual IEnumerable? Children => _content.Children; - /// - public virtual IEnumerable Properties => _content.Properties; + /// + public virtual IEnumerable? ChildrenForAllCultures => _content.ChildrenForAllCultures; - /// - public virtual IPublishedProperty? GetProperty(string alias) - { - return _content.GetProperty(alias); - } + /// + public virtual IEnumerable Properties => _content.Properties; - #endregion - } + /// + 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 9525a9d7ac8e..1101301f3616 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)); + throw new ArgumentNullException(nameof(name)); + } - Culture = culture ?? throw new ArgumentNullException(nameof(culture)); - Name = name; - UrlSegment = urlSegment; - Date = date; + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// 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; } + 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 de590c2531a7..8f77f404ae4d 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) - { - _lazyConfiguration = lazyConfiguration; - - Id = id; - EditorAlias = editorAlias; - } + Id = id; + EditorAlias = editorAlias; + } - /// - /// Gets the datatype identifier. - /// - public int Id { get; } + /// + /// Gets the datatype identifier. + /// + public int Id { get; } - /// - /// Gets the data type editor alias. - /// - public string EditorAlias { get; } + /// + /// Gets the data type editor alias. + /// + public string EditorAlias { get; } - /// - /// Gets the data type configuration. - /// - public object? Configuration => _lazyConfiguration?.Value; + /// + /// 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 + /// + /// 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) { - 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}."); + 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}."); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedElementModel.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedElementModel.cs index f093e7b20c18..b91171012cc0 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 cc0c6b963aca..d56230cbfad0 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; - } + _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 IPublishedContentType ContentType => _content.ContentType; + /// + public Guid Key => _content.Key; - /// - public Guid Key => _content.Key; + /// + public IEnumerable Properties => _content.Properties; - /// - public IEnumerable Properties => _content.Properties; + /// + public IPublishedProperty? GetProperty(string alias) => _content.GetProperty(alias); - /// - 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 7d16152b6ee7..2204cc5107d1 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 035c8a213a48..5048f6190860 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)); + throw new ArgumentNullException(nameof(contentTypeAlias)); + } - ContentTypeAlias = contentTypeAlias; + if (string.IsNullOrWhiteSpace(contentTypeAlias)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(contentTypeAlias)); } - /// - /// Gets or sets the content type alias. - /// - public string ContentTypeAlias { get; } + 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 7053a238e677..b2d5da7876b2 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; - - private class ModelInfo - { - public Type? ParameterType { get; set; } - - public Func? Ctor { get; set; } - - public Type? ModelType { get; set; } - - public Func? ListCtor { get; set; } - } + var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + var modelTypeMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - /// - /// 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) + foreach (Type type in types) { - var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - var modelTypeMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + // 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 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."); } - } - if (constructor == null) - { - throw new InvalidOperationException($"Type {type.FullName} is missing a public constructor with one argument of type, or implementing, IPublishedElement."); + constructor = ctor; + parameterType = parms[0].ParameterType; } - - 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; - } - - /// - public IPublishedElement CreateModel(IPublishedElement element) - { - // fail fast - if (_modelInfos is null || element.ContentType.Alias is null || !_modelInfos.TryGetValue(element.ContentType.Alias, out var modelInfo)) + if (constructor == null) { - return element; + throw new InvalidOperationException( + $"Type {type.FullName} is missing a public constructor with one argument of type, or implementing, IPublishedElement."); } - // ReSharper disable once UseMethodIsInstanceOfType - if (modelInfo.ParameterType?.IsAssignableFrom(element.GetType()) == false) + PublishedModelAttribute? attribute = type.GetCustomAttribute(false); + var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; + + if (modelInfos.TryGetValue(typeName, out ModelInfo? modelInfo)) { - throw new InvalidOperationException($"Model {modelInfo.ModelType} expects argument of type {modelInfo.ParameterType.FullName}, but got {element.GetType().FullName}."); + throw new InvalidOperationException( + $"Both types '{type.AssemblyQualifiedName}' and '{modelInfo.ModelType?.AssemblyQualifiedName}' want to be a model type for content type with alias \"{typeName}\"."); } - // can cast, because we checked when creating the ctor - return (IPublishedElement)modelInfo.Ctor!(element, _publishedValueFallback); + // 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 IList? CreateModelList(string? alias) + _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 || alias is null || !_modelInfos.TryGetValue(alias, out var modelInfo) || modelInfo.ModelType is null) - { - return new List(); - } + return element; + } - var ctor = modelInfo.ListCtor; - if (ctor != null) - { - return ctor(); - } + // 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}."); + } - var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); - ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType); - if (ctor is not null) - { - return ctor(); - } + // can cast, because we checked when creating the ctor + return (IPublishedElement)modelInfo.Ctor!(element, _publishedValueFallback); + } - return null; + /// + public IList? CreateModelList(string? alias) + { + // fail fast + if (_modelInfos is null || alias is null || !_modelInfos.TryGetValue(alias, out ModelInfo? modelInfo) || + modelInfo.ModelType is null) + { + return new List(); } - /// - public Type GetModelType(string? alias) + Func? ctor = modelInfo.ListCtor; + if (ctor != null) { - // fail fast - if (_modelInfos is null || - alias is null || - !_modelInfos.TryGetValue(alias, out var modelInfo) || - modelInfo.ModelType is null) - { - return typeof(IPublishedElement); - } + return ctor(); + } - return modelInfo.ModelType; + Type listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); + ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType); + if (ctor is not null) + { + return ctor(); } - /// - public Type MapModelType(Type type) - => ModelType.Map(type, _modelTypeMap); + 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 6cdbd85c7483..25cf64899b8e 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; + PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); + ReferenceCacheLevel = referenceCacheLevel; - ValidateCacheLevel(ReferenceCacheLevel, true); - ValidateCacheLevel(PropertyType.CacheLevel, false); - } + ValidateCacheLevel(ReferenceCacheLevel, true); + ValidateCacheLevel(PropertyType.CacheLevel, false); + } - // 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 reference cache level. + /// + public PropertyCacheLevel ReferenceCacheLevel { get; } - /// - /// Gets the property type. - /// - public IPublishedPropertyType PropertyType { get; } + /// + /// Gets the property type. + /// + public IPublishedPropertyType PropertyType { get; } - /// - /// Gets the property reference cache level. - /// - public PropertyCacheLevel ReferenceCacheLevel { get; } + /// + public string Alias => PropertyType.Alias; - /// - public string Alias => PropertyType.Alias; + /// + public abstract bool HasValue(string? culture = null, string? segment = null); - /// - public abstract bool HasValue(string? culture = null, string? segment = null); + /// + public abstract object? GetSourceValue(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? GetValue(string? culture = null, string? segment = null); + /// + public abstract object? GetXPathValue(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) + { + 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}\"."); + } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index 9420811f24f6..4bc4b02f6823 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 @@ public PublishedPropertyType(string propertyTypeAlias, int dataTypeId, bool isUs private void Initialize() { - if (_initialized) return; + if (_initialized) + { + return; + } + lock (_locker) { - if (_initialized) return; + if (_initialized) + { + return; + } + InitializeLocked(); _initialized = true; } @@ -113,10 +120,12 @@ private void InitializeLocked() _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 @@ private void InitializeLocked() 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 @@ private void InitializeLocked() 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 @@ private void InitializeLocked() /// 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 @@ public PropertyCacheLevel CacheLevel { get { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } + return _cacheLevel; } } @@ -204,7 +228,10 @@ public PropertyCacheLevel CacheLevel /// 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 @@ public PropertyCacheLevel CacheLevel /// 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 @@ public PropertyCacheLevel CacheLevel /// 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 @@ public Type ModelClrType { get { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } + return _modelClrType!; } } @@ -254,7 +300,11 @@ public Type? ClrType { 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 edc6cd915024..f0c2626f906c 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 ed8acf27367a..64f0160383e1 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) - { - _localizationService = serviceContext.LocalizationService; - _variationContextAccessor = 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, 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); - /// - public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) + foreach (var f in fallback) { - _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) - { - return TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value); - } + _variationContextAccessor.ContextualizeVariation(propertyType.Variations, ref culture, ref segment); - /// - public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) + foreach (var f in fallback) { - var propertyType = content.ContentType.GetPropertyType(alias); - if (propertyType == null) + switch (f) { - value = default; - return false; + 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"); } + } - _variationContextAccessor.ContextualizeVariation(propertyType.Variations, ref culture, ref segment); + value = default; + return false; + } - foreach (var f in fallback) - { - 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(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); - value = default; - return false; - } + /// + public virtual bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value, out IPublishedProperty? noValueProperty) + { + noValueProperty = default; - /// - public bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value, out IPublishedProperty? noValueProperty) + IPublishedPropertyType? propertyType = content.ContentType.GetPropertyType(alias); + if (propertyType != null) { - return TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value, out noValueProperty); + _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); + noValueProperty = content.GetProperty(alias); } - /// - public virtual bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value, out IPublishedProperty? noValueProperty) + // 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) { - noValueProperty = default; - - var propertyType = content.ContentType.GetPropertyType(alias); - if (propertyType != null) + switch (f) { - _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); - noValueProperty = content.GetProperty(alias); - } + case Fallback.None: + continue; + case Fallback.DefaultValue: + value = defaultValue; + return true; + case Fallback.Language: + if (propertyType == null) + { + continue; + } - // 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. + if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value)) + { + return true; + } - foreach (var f in fallback) - { - switch (f) - { - case Fallback.None: - continue; - case Fallback.DefaultValue: - value = defaultValue; + break; + case Fallback.Ancestors: + if (TryGetValueWithAncestorsFallback(content, alias, culture, segment, out value, ref noValueProperty)) + { 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; + break; + default: + throw NotSupportedFallbackMethod(f, "content"); + } } - private NotSupportedException NotSupportedFallbackMethod(int fallback, string level) - { - return new NotSupportedException($"Fallback {GetType().Name} does not support fallback code '{fallback}' at {level} level."); - } + 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) + // 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 { - 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) { - 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; - } + culture = null; + segment = null; + _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); } - 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)) + property = content?.GetProperty(alias); + if (property != null && noValueProperty == null) { - value = property.Value(this, culture, segment); - return true; + noValueProperty = property; } + } + while (content != null && (property == null || property.HasValue(culture, segment) == false)); - value = default; - return 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; } - // tries to get a value, falling back onto other languages - private bool TryGetValueWithLanguageFallback(IPublishedProperty property, string? culture, string? segment, out T? value) + 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()) { - value = default; + return false; + } - if (culture.IsNullOrWhiteSpace()) return false; + var visited = new HashSet(); - var visited = new HashSet(); + ILanguage? language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; + if (language == null) + { + return false; + } - var language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; - if (language == null) return false; + while (true) + { + if (language.FallbackLanguageId == null) + { + return false; + } - while (true) + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) { - if (language.FallbackLanguageId == null) return false; + return false; + } - var language2Id = language.FallbackLanguageId.Value; - if (visited.Contains(language2Id)) return false; - visited.Add(language2Id); + visited.Add(language2Id); - var language2 = _localizationService?.GetLanguageById(language2Id); - if (language2 == null) return false; - var culture2 = language2.IsoCode; + ILanguage? language2 = _localizationService?.GetLanguageById(language2Id); + if (language2 == null) + { + return false; + } - if (property.HasValue(culture2, segment)) - { - value = property.Value(this, culture2, segment); - return true; - } + var culture2 = language2.IsoCode; - language = language2; + if (property.HasValue(culture2, segment)) + { + value = property.Value(this, culture2, segment); + return true; } + + 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; - // tries to get a value, falling back onto other languages - private bool TryGetValueWithLanguageFallback(IPublishedElement content, string alias, string? culture, string? segment, out T? value) + if (culture.IsNullOrWhiteSpace()) { - value = default; + return false; + } - if (culture.IsNullOrWhiteSpace()) return false; + var visited = new HashSet(); - var visited = new HashSet(); + ILanguage? language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; + if (language == null) + { + return false; + } - var language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; - if (language == null) return false; + while (true) + { + if (language.FallbackLanguageId == null) + { + return false; + } - while (true) + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) { - if (language.FallbackLanguageId == null) return false; + return false; + } - var language2Id = language.FallbackLanguageId.Value; - if (visited.Contains(language2Id)) return false; - visited.Add(language2Id); + visited.Add(language2Id); - var language2 = _localizationService?.GetLanguageById(language2Id); - if (language2 == null) return false; - var culture2 = language2.IsoCode; + ILanguage? language2 = _localizationService?.GetLanguageById(language2Id); + if (language2 == null) + { + return false; + } - if (content.HasValue(alias, culture2, segment)) - { - value = content.Value(this, alias, culture2, segment); - return true; - } + var culture2 = language2.IsoCode; - language = language2; + 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; + // 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; + if (culture.IsNullOrWhiteSpace()) + { + return false; + } - var visited = new HashSet(); + 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 + // 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) + { + return false; + } - var language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; - if (language == null) return false; + while (true) + { + if (language.FallbackLanguageId == null) + { + return false; + } - while (true) + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) { - if (language.FallbackLanguageId == null) return false; + return false; + } - var language2Id = language.FallbackLanguageId.Value; - if (visited.Contains(language2Id)) return false; - visited.Add(language2Id); + visited.Add(language2Id); - var language2 = _localizationService?.GetLanguageById(language2Id); - if (language2 == null) return false; - var culture2 = language2.IsoCode; + ILanguage? language2 = _localizationService?.GetLanguageById(language2Id); + if (language2 == null) + { + return false; + } - if (content.HasValue(alias, culture2, segment)) - { - value = content.Value(this, alias, culture2, segment); - return true; - } + var culture2 = language2.IsoCode; - language = language2; + 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 2ae0ce6c1dbf..763006f8f1a7 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 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; + private readonly Lazy _objectValue; + private readonly object _sourceValue; // the value in the db + private readonly Lazy _xpathValue; - public override bool HasValue(string? culture = null, string? segment = 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) { - 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; - - public override object? GetXPathValue(string? culture = null, string? segment = null) - => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _xpathValue.Value : null; + _sourceValue = sourceValue; - 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)); + 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)); + } - _sourceValue = sourceValue; + // 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; - 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)); - } + 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 a9d06e521f8f..591937079262 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 8e24f25332c6..ff13964fb3e5 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 9b8ae302457f..92326ae35955 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 - } + 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 culture. + /// + public string Culture { get; } - /// - /// Gets the segment. - /// - public string Segment { get; } + /// + /// Gets the segment. + /// + public string Segment { get; } - /// - /// Gets the segment for the content item - /// - /// - /// - public virtual string GetSegment(int contentId) => Segment; - } + /// + /// 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 4a986597bd6e..e8f6e3bdc1a7 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, + 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); + 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) + 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 87c106e11ec5..39d68ea27300 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 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, + + // 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: + + /// + /// 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 states of a content item. + /// The version is being saved, in order to unpublish the content. /// - 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 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, - // 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: - - /// - /// 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 + /// 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 9c5da2087e50..78d49ad8514a 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 cbb1e51a3e84..77b6253178d7 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 a57a5ba7e1b6..4388499e98de 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 d4acc0b66d64..ed0cde70bd55 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; - } - - private int _contentId; - private Guid _contentKey; - private DateTime _createDateUtc; - private string? _culture; - private string _url; + CreateDateUtc = DateTime.UtcNow; + _url = string.Empty; + } - /// - 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 54227db9108f..c495ed39fb4c 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 { - /// - /// Represents a Relation between two items - /// - [Serializable] - [DataContract(IsReference = true)] - public class Relation : EntityBase, IRelation - { - //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; + private int _childId; - /// - /// Constructor for constructing the entity to be created - /// - /// - /// - /// - public Relation(int parentId, int childId, IRelationType relationType) - { - _parentId = parentId; - _childId = childId; - _relationType = relationType; - } + private string? _comment; - /// - /// 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; - } + // NOTE: The datetime column from umbracoRelation is set on CreateDate on the Entity + private int _parentId; + private IRelationType _relationType; + /// + /// Constructor for constructing the entity to be created + /// + /// + /// + /// + public Relation(int parentId, int childId, IRelationType relationType) + { + _parentId = parentId; + _childId = childId; + _relationType = relationType; + } - /// - /// Gets or sets the Parent Id of the Relation (Source) - /// - [DataMember] - public int ParentId - { - get => _parentId; - set => SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); - } + /// + /// 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; + } - [DataMember] - public Guid ParentObjectType { get; set; } + /// + /// Gets or sets the Parent Id of the Relation (Source) + /// + [DataMember] + public int ParentId + { + get => _parentId; + set => SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); + } - /// - /// 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 ParentObjectType { get; set; } - [DataMember] - public Guid ChildObjectType { 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)); + } - /// - /// Gets or sets the for the Relation - /// - [DataMember] - public IRelationType RelationType - { - get => _relationType; - set => SetPropertyValueAndDetectChanges(value, ref _relationType!, nameof(RelationType)); - } + [DataMember] + public Guid ChildObjectType { get; set; } - /// - /// Gets or sets a comment for the Relation - /// - [DataMember] - public string? Comment - { - get => _comment; - set => SetPropertyValueAndDetectChanges(value, ref _comment, nameof(Comment)); - } + /// + /// Gets or sets the for the Relation + /// + [DataMember] + public IRelationType RelationType + { + get => _relationType; + set => SetPropertyValueAndDetectChanges(value, ref _relationType!, nameof(RelationType)); + } - /// - /// Gets the Id of the that this Relation is based on. - /// - [IgnoreDataMember] - public int RelationTypeId => _relationType.Id; + /// + /// 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 75344914f04b..409776b7e39f 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 -{ - [DataContract(Name = "relationItem", Namespace = "")] - public class RelationItem - { - [DataMember(Name = "id")] - public int NodeId { get; set; } +namespace Umbraco.Cms.Core.Models; - [DataMember(Name = "key")] - public Guid NodeKey { get; set; } +[DataContract(Name = "relationItem", Namespace = "")] +public class RelationItem +{ + [DataMember(Name = "id")] + public int NodeId { get; set; } - [DataMember(Name = "name")] - public string? NodeName { get; set; } + [DataMember(Name = "key")] + public Guid NodeKey { get; set; } - [DataMember(Name = "type")] - public string? NodeType { get; set; } + [DataMember(Name = "name")] + public string? NodeName { get; set; } - [DataMember(Name = "udi")] - public Udi NodeUdi => Udi.Create(NodeType, NodeKey); + [DataMember(Name = "type")] + public string? NodeType { get; set; } - [DataMember(Name = "icon")] - public string? ContentTypeIcon { get; set; } + [DataMember(Name = "udi")] + public Udi NodeUdi => Udi.Create(NodeType, NodeKey); - [DataMember(Name = "alias")] - public string? ContentTypeAlias { get; set; } + [DataMember(Name = "icon")] + public string? ContentTypeIcon { get; set; } - [DataMember(Name = "contentTypeName")] - public string? ContentTypeName { get; set; } + [DataMember(Name = "alias")] + public string? ContentTypeAlias { get; set; } - [DataMember(Name = "relationTypeName")] - public string? RelationTypeName { get; set; } + [DataMember(Name = "contentTypeName")] + public string? ContentTypeName { get; set; } - [DataMember(Name = "relationTypeIsBidirectional")] - public bool RelationTypeIsBidirectional { get; set; } + [DataMember(Name = "relationTypeName")] + public string? RelationTypeName { get; set; } - [DataMember(Name = "relationTypeIsDependency")] - public bool RelationTypeIsDependency { get; set; } + [DataMember(Name = "relationTypeIsBidirectional")] + public bool RelationTypeIsBidirectional { 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 4c4c69c5f18e..d48e802c6eaa 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) + { + } - [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)); } - public RelationType(string? name, string? alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType, bool isDependency) + 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 (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 ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Gets or sets the Name of the RelationType - /// - [DataMember] - public string? Name + if (alias == null) { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + throw new ArgumentNullException(nameof(alias)); } - /// - /// Gets or sets the Alias of the RelationType - /// - [DataMember] - public string Alias + if (string.IsNullOrWhiteSpace(alias)) { - get => _alias; - set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(alias)); } - /// - /// 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)); - } + _name = name; + _alias = alias; + _isBidirectional = isBidrectional; + _isDependency = isDependency; + _parentObjectType = parentObjectType; + _childObjectType = childObjectType; + } - /// - /// 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 Name of the RelationType + /// + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + } - /// - /// 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 the Alias of the RelationType + /// + [DataMember] + public string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); + } + /// + /// 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)); + } - public bool IsDependency - { - get => _isDependency; - set => SetPropertyValueAndDetectChanges(value, ref _isDependency, nameof(IsDependency)); - } + /// + /// 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 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 1e7282b66bbe..b5803d3fb356 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 438e97fb30bc..9b4932f88a36 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 0d121368f860..03888bd27a42 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 783bcdeec28c..c73fd73eb3e7 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 { - /// - /// 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 = "code", IsRequired = true)] + public string? Code { get; set; } - [Required] - [DataMember(Name = "provider", IsRequired = true)] - public string? Provider { 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 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 553460eb5bc4..6507d5d64c34 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; - } + /// + /// 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; + } - /// - /// Gets or sets the server URL. - /// - public string? ServerAddress - { - get => _serverAddress; - set => SetPropertyValueAndDetectChanges(value, ref _serverAddress, nameof(ServerAddress)); - } + /// + /// 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 unique identity. - /// - public string? ServerIdentity - { - get => _serverIdentity; - set => SetPropertyValueAndDetectChanges(value, ref _serverIdentity, nameof(ServerIdentity)); - } + /// + /// Gets or sets the server URL. + /// + public string? ServerAddress + { + get => _serverAddress; + set => SetPropertyValueAndDetectChanges(value, ref _serverAddress, nameof(ServerAddress)); + } - /// - /// 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 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 has the SchedulingPublisher role - /// - public bool IsSchedulingPublisher - { - get => _isSchedulingPublisher; - set => SetPropertyValueAndDetectChanges(value, ref _isSchedulingPublisher, nameof(IsSchedulingPublisher)); - } + /// + /// Gets or sets a value indicating whether the server is active. + /// + public bool IsActive + { + get => _isActive; + set => SetPropertyValueAndDetectChanges(value, ref _isActive, nameof(IsActive)); + } - /// - /// Gets the date and time the registration was created. - /// - public DateTime Registered - { - get => CreateDate; - set => CreateDate = value; - } + /// + /// 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 last accessed. - /// - public DateTime Accessed - { - get => UpdateDate; - set => UpdateDate = value; - } + /// + /// Gets the date and time the registration was created. + /// + public DateTime Registered + { + get => CreateDate; + set => CreateDate = 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 ? "" : "!"); - } + /// + /// 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 c904f98694fd..57d1abc38f94 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 31e061362cd1..7fe88a8a8ae9 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) - { - DefaultTemplate = contentType.DefaultTemplate; - } + } + + /// + /// Initializes a new instance of the class. + /// + public SimpleContentType(IMemberType memberType) + : this((IContentTypeBase)memberType) + { + } - /// - /// Initializes a new instance of the class. - /// - public SimpleContentType(IMediaType mediaType) - : this((IContentTypeBase)mediaType) - { } - - /// - /// Initializes a new instance of the class. - /// - public SimpleContentType(IMemberType memberType) - : this((IContentTypeBase)memberType) - { } - - private SimpleContentType(IContentTypeBase contentType) + private SimpleContentType(IContentTypeBase contentType) + { + if (contentType == null) { - 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; + throw new ArgumentNullException(nameof(contentType)); } - public string Alias { get; } + 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; + } + + public string Alias { get; } + + public int Id { get; } + + public Guid Key { get; } - public int Id { get; } + /// + public ITemplate? DefaultTemplate { get; } - public Guid Key { get; } + public ContentVariation Variations { get; } - /// - public ITemplate? DefaultTemplate { get; } + public string? Icon { get; } - public ContentVariation Variations { get; } + public bool IsContainer { get; } - public string? Icon { get; } + public string? Name { get; } - public bool IsContainer { get; } + public bool AllowedAsRoot { get; } - public string? Name { get; } + public bool IsElement { get; } - public bool AllowedAsRoot { get; } + public bool SupportsPropertyVariation(string? culture, string segment, bool wildcards = false) => - public bool IsElement { get; } + // 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 bool SupportsPropertyVariation(string? culture, string segment, bool wildcards = false) + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, 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 false; } - protected bool Equals(SimpleContentType other) + if (ReferenceEquals(this, obj)) { - return string.Equals(Alias, other.Alias) && Id == other.Id; + return true; } - public override bool Equals(object? obj) + if (obj.GetType() != GetType()) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((SimpleContentType) obj); + return false; } - public override int GetHashCode() + return Equals((SimpleContentType)obj); + } + + 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 30efec7dfe06..390fe5a31cfa 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 7b1d971434b9..07f35c88e317 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 { + private Lazy>? _properties; + + public Stylesheet(string path) + : this(path, null) + { + } + + public Stylesheet(string path, Func? getFileContent) + : base(string.IsNullOrEmpty(path) ? path : path.EnsureEndsWith(".css"), getFileContent) => + InitializeProperties(); + /// - /// Represents a Stylesheet file + /// Gets or sets the Content of a File /// - [Serializable] - [DataContract(IsReference = true)] - public class Stylesheet : File, IStylesheet + public override string? Content { - public Stylesheet(string path) - : this(path, null) - { } - - public Stylesheet(string path, Func? getFileContent) - : base(string.IsNullOrEmpty(path) ? path : path.EnsureEndsWith(".css"), getFileContent) + 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() - { - //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); - } - } + /// + /// 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; - //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(); - }); + /// + /// 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"); } - /// - /// 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) - { - //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 - }); - } - } + // re-set lazy collection + InitializeProperties(); + } - /// - /// Gets or sets the Content of a File - /// - public override string? Content + /// + /// Removes an Umbraco stylesheet property + /// + /// + public void RemoveProperty(string name) + { + if (Properties is not null && Properties.Any(x => x.Name.InvariantEquals(name))) { - get { return base.Content; } - set - { - base.Content = value; - //re-set the properties so they are re-read from the content - InitializeProperties(); - } + Content = StylesheetHelper.ReplaceRule(Content, name, null); } + } - /// - /// 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 + 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) { - get { return _properties?.Value; } - } + // 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(); - /// - /// 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))) + // update existing + foreach (StylesheetProperty stylesheetProperty in existing) { - throw new DuplicateNameException("The property with the name " + property.Name + " already exists in the collection"); + 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; } - //now we need to serialize out the new property collection over-top of the string Content. - Content = StylesheetHelper.AppendRule(Content, new StylesheetRule + // remove no longer existing + StylesheetProperty[] nonExisting = + _properties.Value.Where(x => names.InvariantContains(x.Name) == false).ToArray(); + foreach (StylesheetProperty stylesheetProperty in nonExisting) { - Name = property.Name, - Selector = property.Alias, - Styles = property.Value - }); + stylesheetProperty.PropertyChanged -= Property_PropertyChanged; + _properties.Value.Remove(stylesheetProperty); + } - //re-set lazy collection - InitializeProperties(); + // 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); + } } - /// - /// Removes an Umbraco stylesheet property - /// - /// - public void RemoveProperty(string name) + // we haven't read the properties yet so create the lazy delegate + _properties = new Lazy>(() => { - if (Properties is not null && Properties.Any(x => x.Name.InvariantEquals(name))) + IEnumerable parsed = StylesheetHelper.ParseRules(Content); + return parsed.Select(statement => { - Content = StylesheetHelper.ReplaceRule(Content, name, null); - } - } + var property = new StylesheetProperty(statement.Name, statement.Selector, statement.Styles); + property.PropertyChanged += Property_PropertyChanged; + return property; + }).ToList(); + }); + } + + /// + /// If the property has changed then we need to update the content + /// + /// + /// + private void Property_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + var prop = (StylesheetProperty?)sender; - /// - /// 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 + if (prop is not null) { - 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 af6f347a63cd..730ff8ff3e20 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 -{ - /// - /// 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; +namespace Umbraco.Cms.Core.Models; - public StylesheetProperty(string name, string @alias, string value) - { - Name = name; - _alias = alias; - _value = value; - } +/// +/// 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; - /// - /// The CSS rule name that can be used by Umbraco in the back office - /// - public string Name { get; private set; } + public StylesheetProperty(string name, string alias, string value) + { + Name = name; + _alias = alias; + _value = value; + } - /// - /// This is the CSS Selector - /// - public string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); - } + /// + /// The CSS rule name that can be used by Umbraco in the back office + /// + public string Name { get; private set; } - /// - /// 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 92436d068b89..1c4bf4b88cd4 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 6a0430a492e1..2646b216e388 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 8a9384ec7436..03be2273a2f4 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 9bc05eae15f6..821f592343c2 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; - } + EntityId = entityId; + TaggedProperties = taggedProperties; + } - /// - /// Gets the identifier of the entity. - /// - public int EntityId { get; } + /// + /// Gets the identifier of the entity. + /// + public int EntityId { get; } - /// - /// Gets the tagged properties. - /// - public IEnumerable TaggedProperties { 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 24ef9ccc4562..90257a1a3e13 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; - } + PropertyTypeId = propertyTypeId; + PropertyTypeAlias = propertyTypeAlias; + Tags = tags; + } - /// - /// Gets the identifier of the property type. - /// - public int PropertyTypeId { get; } + /// + /// Gets the identifier of the property type. + /// + public int PropertyTypeId { get; } - /// - /// Gets the alias of the property type. - /// - public string? PropertyTypeAlias { get; } + /// + /// Gets the alias of the property type. + /// + public string? PropertyTypeAlias { get; } - /// - /// Gets the tags. - /// - public IEnumerable Tags { 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 7bd8ea7937a5..ccff41bb72ec 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 26a714b38578..cdf1d24e90d1 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 401e07848f58..1c6284238190 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 7efccf1e7d22..1900233aa989 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 string _alias; - private readonly IShortStringHelper _shortStringHelper; - private string? _name; - private string? _masterTemplateAlias; - private Lazy? _masterTemplateId; + 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) - { } + 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) + { + _shortStringHelper = shortStringHelper; + _name = name; + _alias = alias?.ToCleanString(shortStringHelper, CleanStringType.UnderscoreAlias) ?? string.Empty; + _masterTemplateId = new Lazy(() => -1); + } - [DataMember] - public Lazy? MasterTemplateId - { - get => _masterTemplateId; - set => SetPropertyValueAndDetectChanges(value, ref _masterTemplateId, nameof(MasterTemplateId)); - } + [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)); - } + 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? 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)); - } + [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; } + /// + /// 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) + public void SetMasterTemplate(ITemplate? masterTemplate) + { + if (masterTemplate == null) { - if (masterTemplate == null) - { - MasterTemplateId = new Lazy(() => -1); - MasterTemplateAlias = null; - } - else - { - MasterTemplateId = new Lazy(() => masterTemplate.Id); - MasterTemplateAlias = masterTemplate.Alias; - } - + MasterTemplateId = new Lazy(() => -1); + MasterTemplateAlias = null; } - - protected override void DeepCloneNameAndAlias(File clone) + else { - // 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 339f4efee319..f02988e6d2b8 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(); - } + Template = template; + Children = new List(); + } - /// - /// The current template - /// - public ITemplate Template { get; set; } + /// + /// The current template + /// + public ITemplate Template { get; set; } - /// - /// The children of the current template - /// - public IEnumerable Children { 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; } - } + /// + /// 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 61c10ba456ed..04fffb7c10ea 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 - { - /// - /// 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) - { - IsOnDisk = true; - } + /// 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 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 + /// + /// 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 => IsOnDisk ? string.Empty : base.Content; + set { - 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 f4f3e7bc59bd..c94cd67b8a4e 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 +namespace Umbraco.Cms.Core.Models.TemplateQuery; + +public class ContentTypeModel { - public class ContentTypeModel - { - public string? Alias { get; set; } + public string? Alias { get; set; } - public string? Name { 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 eb3fe4be29bb..c76202fb68a7 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 a8e3b40fefb7..fc23ebdb3d5c 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 ce66965c689d..d2a8c8e0dbbd 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; - } + Name = "is"; + Operator = Operator.Equals; + AppliesTo = new[] { "string" }; + } - public string Name { get; set; } - public Operator Operator { get; set; } - public IEnumerable AppliesTo { get; set; } + 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 3ea4059b7e69..39ea100e7d45 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 b6305f16a8ec..2c64f13876ce 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 962cf9255854..0722422aaef8 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 Lazy(() => typeof(string).GetMethod("Contains", new[] {typeof(string)})!); + private static Lazy StringContainsMethodInfo => + new(() => typeof(string).GetMethod("Contains", new[] { typeof(string) })!); - public static Expression> BuildCondition(this QueryCondition condition, string parameterAlias) + 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; - } + 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; + } - var parameterExpression = Expression.Parameter(typeof(T), parameterAlias); - var propertyExpression = Expression.Property(parameterExpression, condition.Property.Alias); + ParameterExpression parameterExpression = Expression.Parameter(typeof(T), parameterAlias); + MemberExpression 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; - } + 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); + var predicate = + Expression.Lambda>(bodyExpression.Reduce(), parameterExpression); - return predicate; - } + return predicate; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/QueryModel.cs b/src/Umbraco.Core/Models/TemplateQuery/QueryModel.cs index 48d6506143c1..06f5c82d1911 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 8605f924235f..61845214a5eb 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 c68b366ba547..b5accd7ccda9 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 +namespace Umbraco.Cms.Core.Models.TemplateQuery; + +public class SortExpression { - public class SortExpression - { - public PropertyModel? Property { get; set; } + public PropertyModel? Property { get; set; } - public string? Direction { 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 4b67f7e73cb9..a36ae38a9e70 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 95615b4d0d9e..4e56beb63543 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 +namespace Umbraco.Cms.Core.Models.TemplateQuery; + +public class TemplateQueryResult { - public class TemplateQueryResult - { - public string? Icon { get; set; } + public string? Icon { get; set; } - public string? Name { 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 c89fb402d071..87fe72a0fbae 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 - { - /// - /// The angular service name containing the - /// - public abstract string AngularServiceName { get; } + protected ActionMenuItem(string alias, string name) + : base(alias, name) => Initialize(); - /// - /// The angular service method name to call for this menu item - /// - public virtual string? AngularServiceMethodName { get; } = null; + protected ActionMenuItem(string alias, ILocalizedTextService textService) + : base(alias, textService) => + Initialize(); - protected ActionMenuItem(string alias, string name) : base(alias, name) - { - Initialize(); - } + /// + /// The angular service name containing the + /// + public abstract string AngularServiceName { get; } - protected ActionMenuItem(string alias, ILocalizedTextService textService) : base(alias, textService) + /// + /// The angular service method name to call for this menu item + /// + public virtual string? AngularServiceMethodName { get; } = null; + + private void Initialize() + { + // 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}"); } - - private void Initialize() + else { - //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 a8d945242e29..41c8c6f0deeb 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; - } + Icon = "add"; + Name = name; + SeparatorBefore = separatorBefore; + } - public CreateChildEntity(ILocalizedTextService textService, bool separatorBefore = false) - : base(ActionNew.ActionAlias, textService) - { - Icon = "add"; - 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 30f904f95291..3f11ef4b0548 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 e56a2440a8ce..3f77ccf2b608 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 { + #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); + } + /// - /// A context menu item + /// Sets the menu item to display a dialog based on a URL path in an iframe /// - [DataContract(Name = "menuItem", Namespace = "")] - public class MenuItem + /// + /// + public void LaunchDialogUrl(string url, string dialogTitle) { - #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 + 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 01eb2fa34adc..befbec019e79 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; - } + Icon = "refresh"; + SeparatorBefore = separatorBefore; + } - public RefreshNode(ILocalizedTextService textService, bool separatorBefore = false) - : base("refreshNode", textService) - { - 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 c38105626c94..551482e3a25a 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 3f2eb00f51ef..c883e147709b 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 + { + 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 { - 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 => _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 00dbd490f8d7..600927db84f0 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 - /// - public enum UmbracoObjectTypes - { - /// - /// Default value - /// - Unknown, - - - /// - /// Root - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.SystemRoot)] - [FriendlyName("Root")] - ROOT, - - /// - /// 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, - - /// - /// 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, - - /// - /// 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, - - /// - /// 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, - - /// - /// 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, - - /// - /// 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.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, - - /// - /// Forms Form - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsForm)] - [FriendlyName("Form")] - FormsForm, - - /// - /// Forms PreValue - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsPreValue)] - [FriendlyName("PreValue")] - FormsPreValue, - - /// - /// Forms DataSource - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsDataSource)] - [FriendlyName("DataSource")] - FormsDataSource, - - /// - /// 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, - - /// - /// Reserved Identifier - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.IdReservation)] - [FriendlyName("Identifier Reservation")] - IdReservation - - } + /// Default value + /// + Unknown, + + /// + /// Root + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.SystemRoot)] + [FriendlyName("Root")] + ROOT, + + /// + /// 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, + + /// + /// 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, + + /// + /// 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, + + /// + /// 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, + + /// + /// 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, + + /// + /// 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.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, + + /// + /// Forms Form + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsForm)] + [FriendlyName("Form")] + FormsForm, + + /// + /// Forms PreValue + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsPreValue)] + [FriendlyName("PreValue")] + FormsPreValue, + + /// + /// Forms DataSource + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsDataSource)] + [FriendlyName("DataSource")] + FormsDataSource, + + /// + /// 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, + + /// + /// 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 71612f353124..d708704fac45 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) + { + 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) { - public static IEnumerable GetPermissions(this IUser user, string path, IUserService userService) + 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) + return GetUserCulture(user.Language, textService, globalSettings); + } + + public static CultureInfo GetUserCulture(string? userLanguage, ILocalizedTextService textService, GlobalSettings globalSettings) + { + try { - if (user == null) throw new ArgumentNullException(nameof(user)); - if (textService == null) throw new ArgumentNullException(nameof(textService)); - return GetUserCulture(user.Language, textService, globalSettings); - } + var culture = CultureInfo.GetCultureInfo(userLanguage!.Replace("_", "-")); - public static CultureInfo GetUserCulture(string? userLanguage, ILocalizedTextService textService, GlobalSettings globalSettings) + // 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) { - 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); - } + // 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 d8c9920c5e0d..c12123081048 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 +namespace Umbraco.Cms.Core.Models; + +public class UnLinkLoginModel { - public class UnLinkLoginModel - { - [Required] - [DataMember(Name = "loginProvider", IsRequired = true)] - public string? LoginProvider { get; set; } + [Required] + [DataMember(Name = "loginProvider", IsRequired = true)] + public string? LoginProvider { get; set; } - [Required] - [DataMember(Name = "providerKey", IsRequired = true)] - public string? ProviderKey { 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 3238720541b2..b639616524e5 100644 --- a/src/Umbraco.Core/Models/UpgradeCheckResponse.cs +++ b/src/Umbraco.Core/Models/UpgradeCheckResponse.cs @@ -2,26 +2,28 @@ 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() + { + } + + public UpgradeCheckResponse(string upgradeType, string upgradeComment, string upgradeUrl, IUmbracoVersion umbracoVersion) { - [DataMember(Name = "type")] - public string? Type { get; set; } + Type = upgradeType; + Comment = upgradeComment; + Url = upgradeUrl + "?version=" + WebUtility.UrlEncode(umbracoVersion.Version?.ToString(3)); + } - [DataMember(Name = "comment")] - public string? Comment { get; set; } + [DataMember(Name = "type")] + public string? Type { get; set; } - [DataMember(Name = "url")] - public string? Url { get; set; } + [DataMember(Name = "comment")] + public string? Comment { 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)); - } - } + [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 e2bedd6f0f0b..3de3a1201aa7 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; } + Name = name; + Data = data; + } - [DataMember(Name = "data")] - public object Data { get; } + [DataMember(Name = "name")] + public string Name { get; } - public UsageInformation(string name, object data) - { - Name = name; - Data = data; - } - } + [DataMember(Name = "data")] + public object Data { get; } } diff --git a/src/Umbraco.Core/Models/UserData.cs b/src/Umbraco.Core/Models/UserData.cs index 07b45b3c547b..144871c3f7d2 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 924b11bcc4ca..87f91978e0b1 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.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()) + // try Gravatar + var gravatarAccess = cache.GetCacheItem("UserAvatar" + user.Id, () => { - var gravatarHash = user.Email?.GenerateHash(); - var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash + "?d=404"; + // Test if we can reach this URL, will fail when there's network or firewall errors + var request = (HttpWebRequest)WebRequest.Create(gravatarUrl); - //try Gravatar - var gravatarAccess = cache.GetCacheItem("UserAvatar" + user.Id, () => + // 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; + }); - //use the custom avatar - var avatarUrl = mediaFileManager.FileSystem.GetUrl(user.Avatar); - return new[] + if (gravatarAccess) { - 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(); + return new[] + { + gravatarUrl + "&s=30", gravatarUrl + "&s=60", gravatarUrl + "&s=90", gravatarUrl + "&s=150", + gravatarUrl + "&s=300", + }; + } + return new string[0]; } - - - internal static bool HasContentRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) + // use the custom avatar + var avatarUrl = mediaFileManager.FileSystem.GetUrl(user.Avatar); + return new[] { - return ContentPermissions.HasPathAccess(Constants.System.RootString, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); - } + 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 HasContentBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) + public static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService, AppCaches appCaches) + { + if (content == null) { - return ContentPermissions.HasPathAccess(Constants.System.RecycleBinContentString, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); + throw new ArgumentNullException(nameof(content)); } - internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) - { - return ContentPermissions.HasPathAccess(Constants.System.RootString, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); - } + return ContentPermissions.HasPathAccess( + content.Path, + user.CalculateContentStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinContent); + } - internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) + 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) { - return ContentPermissions.HasPathAccess(Constants.System.RecycleBinMediaString, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); + throw new ArgumentNullException(nameof(media)); } - 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); - } + return ContentPermissions.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); + } - public static bool HasPathAccess(this IUser user, IMedia? media, IEntityService entityService, AppCaches appCaches) + public static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) + { + if (entity == null) { - if (media == null) throw new ArgumentNullException(nameof(media)); - return ContentPermissions.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); + throw new ArgumentNullException(nameof(entity)); } - 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); - } + 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) + public static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - return ContentPermissions.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); + throw new ArgumentNullException(nameof(entity)); } - /// - /// 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); - } + return ContentPermissions.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); + } - /// - /// 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) + /// + /// Determines whether this user has access to view sensitive data + /// + /// + public static bool HasAccessToSensitiveData(this IUser user) + { + if (user == null) { - var cacheKey = CacheKeys.UserAllContentStartNodesPrefix + user.Id; - var 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 vals = CombineStartNodes(UmbracoObjectTypes.Document, gsn, usn, entityService); - return vals; - } - - return null; - }, TimeSpan.FromMinutes(2), true); - - return result; + throw new ArgumentNullException("user"); } - /// - /// 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; - 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; - } + return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.SensitiveDataGroupAlias); + } - public static string[]? GetMediaStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) + /// + /// 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, + () => { - var cacheKey = CacheKeys.UserMediaStartNodePathsPrefix + user.Id; - var 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 test.StartsWith(path) && test.Length > path.Length && test[path.Length] == ','; - } + return result; + } - private static string GetBinPath(UmbracoObjectTypes objectType) + 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, + () => { - 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.CalculateMediaStartNodeIds(entityService, appCaches); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Media, startNodeIds).Select(x => x.Path).ToArray(); + return vals; + }, + TimeSpan.FromMinutes(2), + true); + + return result; + } - internal static int[] CombineStartNodes(UmbracoObjectTypes objectType, int[] groupSn, int[] userSn, IEntityService entityService) + 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, + () => { - // assume groupSn and userSn each don't contain duplicates + var startNodeIds = user.CalculateContentStartNodeIds(entityService, appCaches); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Document, startNodeIds).Select(x => x.Path) + .ToArray(); + return vals; + }, + TimeSpan.FromMinutes(2), + true); + + return result; + } - 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(); + 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 + paths[Constants.System.Root] = Constants.System.RootString; // entityService does not get that one - var binPath = GetBinPath(objectType); + var binPath = GetBinPath(objectType); - var lsn = new List(); - foreach (var sn in groupSn) + 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 + continue; // ignore rogue node (no path) + } - 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); + if (StartsWithPath(snp, binPath)) + { + continue; // ignore bin } - var usn = new List(); - foreach (var sn in userSn) + if (lsn.Any(x => StartsWithPath(snp, paths[x]))) { - if (paths.TryGetValue(sn, out var snp) == false) continue; // ignore rogue node (no path) + continue; // skip if something above this sn + } - if (StartsWithPath(snp, binPath)) continue; // ignore bin + 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 (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); + if (StartsWithPath(snp, binPath)) + { + continue; // ignore bin } - foreach (var sn in usn) + if (usn.Any(x => StartsWithPath(paths[x], snp))) { - 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 below this sn } - return lsn.ToArray(); + 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 72e0a81cbafd..a954a0b86443 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 + /// + [DataMember(Name = "alias")] + public string Alias { get; set; } = string.Empty; + + /// + /// If the tour is completed /// - [DataContract(Name = "userTourStatus", Namespace = "")] - public class UserTourStatus : IEquatable + [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 string.Equals(Alias, other.Alias); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - return Alias.GetHashCode(); + return false; } - public static bool operator ==(UserTourStatus? left, UserTourStatus? right) + if (ReferenceEquals(this, obj)) { - return Equals(left, right); + return true; } - public static bool operator !=(UserTourStatus? left, UserTourStatus? right) + if (obj.GetType() != GetType()) { - return !Equals(left, right); + 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 095d4f50a954..acdaed7dd9ae 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; - } + ProviderName = providerName; + IsEnabledOnUser = isEnabledOnUser; + } - [DataMember(Name = "providerName")] - public string ProviderName { get; } + [DataMember(Name = "providerName")] + public string ProviderName { get; } - [DataMember(Name = "isEnabledOnUser")] - public bool IsEnabledOnUser { 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 d104383b38c7..b4ebb89e47ec 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 +namespace Umbraco.Cms.Core.Models; + +[Serializable] +[DataContract(Name = "validatePasswordReset", Namespace = "")] +public class ValidatePasswordResetCodeModel { - [Serializable] - [DataContract(Name = "validatePasswordReset", Namespace = "")] - public class ValidatePasswordResetCodeModel - { - [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 = "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/Validation/RequiredForPersistenceAttribute.cs b/src/Umbraco.Core/Models/Validation/RequiredForPersistenceAttribute.cs index 10133a7f3694..bffd5518153a 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 cca84b72b795..975369f9939c 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. - - /// - /// Store property value as NText. - /// - [EnumMember] - 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 11651aaa6ce4..45dbdbbd109a 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; - - /// - /// 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) - { - _locker = locker; - _entered = false; - System.Threading.Monitor.Enter(_locker, ref _entered); - } + _locker = locker; + _entered = false; + Monitor.Enter(_locker, ref _entered); + } - void IDisposable.Dispose() + void IDisposable.Dispose() + { + if (_entered) { - 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 5855f279266a..e0d52df9f4fd 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; - } + /// + /// 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; } - } + /// + /// 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 6c7ab72deca1..edc9c6428c86 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 f5d6b4de290c..4ec6248b3958 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 ba4f61b89796..6916ee8d374b 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 207a9c6048bc..c76c6c86322e 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 eb596a3a0b96..cd0b1326f463 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 adcf14d6364b..23438827fdd6 100644 --- a/src/Umbraco.Core/Notifications/AssignedMemberRolesNotification.cs +++ b/src/Umbraco.Core/Notifications/AssignedMemberRolesNotification.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class AssignedMemberRolesNotification : MemberRolesNotification { - public class AssignedMemberRolesNotification : MemberRolesNotification + public AssignedMemberRolesNotification(int[] memberIds, string[] roles) + : base(memberIds, roles) { - 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 18425f23933d..347f1934bc3f 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 +namespace Umbraco.Cms.Core.Notifications; + +public class AssignedUserGroupPermissionsNotification : EnumerableObjectNotification { - public class AssignedUserGroupPermissionsNotification : EnumerableObjectNotification + public AssignedUserGroupPermissionsNotification(IEnumerable target, EventMessages messages) + : base(target, messages) { - public AssignedUserGroupPermissionsNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable EntityPermissions => Target; } + + public IEnumerable EntityPermissions => Target; } diff --git a/src/Umbraco.Core/Notifications/CacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/CacheRefresherNotification.cs index bd110ad8786c..637c05dfb099 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 +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Base class for cache refresher notifications +/// +public abstract class CacheRefresherNotification : INotification { - /// - /// Base class for cache refresher notifications - /// - public abstract class CacheRefresherNotification : INotification + public CacheRefresherNotification(object messageObject, MessageType messageType) { - public CacheRefresherNotification(object messageObject, MessageType messageType) - { - MessageObject = messageObject ?? throw new ArgumentNullException(nameof(messageObject)); - MessageType = messageType; - } - - public object MessageObject { get; } - public MessageType MessageType { get; } + 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 ea7476cd3f73..1f51e68409fe 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 13989d50dad2..438bc1ee99d3 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 CancelableNotification(EventMessages messages) => Messages = messages; - public EventMessages Messages { get; } + public EventMessages Messages { get; } - public bool Cancel { get; set; } + public bool Cancel { get; set; } - public void CancelOperation(EventMessage cancellationMessage) - { - Cancel = true; - cancellationMessage.IsDefaultEventMessage = true; - Messages.Add(cancellationMessage); - } + public void CancelOperation(EventMessage 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 25f6a4474fe0..be15626eb035 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 35b4f472c758..67a43b5ac264 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 6399fb714d6c..a5c6ede43293 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 d30d49efeb40..ef8eb48058f6 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 1c516a295fe9..884fcf493b79 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 6398c4f28e30..c68a07b1f02e 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 30f00b52bff5..5e2b646008b7 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 ee02c6f339dd..de4176a01b5d 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentDeletingNotification : DeletingNotification { - public sealed class ContentDeletingNotification : DeletingNotification + public ContentDeletingNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentDeletingNotification(IContent target, EventMessages messages) : base(target, messages) - { - } + } - public ContentDeletingNotification(IEnumerable 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 340aaaa55914..5d173bcc0cf1 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 1453553efa64..9a1637dda95d 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 134e65d98295..f55d1166ce5f 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 607d6780493b..50bd24876d3d 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentMovedNotification : MovedNotification { - public sealed class ContentMovedNotification : MovedNotification + public ContentMovedNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) { - public ContentMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } + } - public ContentMovedNotification(IEnumerable> 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 3b736b140959..bf5415d9d1ed 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentMovedToRecycleBinNotification : MovedToRecycleBinNotification { - public sealed class ContentMovedToRecycleBinNotification : MovedToRecycleBinNotification + public ContentMovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base( + target, + messages) { - public ContentMovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } + } - public ContentMovedToRecycleBinNotification(IEnumerable> 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 01c04eb226a5..eddc7a13f786 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentMovingNotification : MovingNotification { - public sealed class ContentMovingNotification : MovingNotification + public ContentMovingNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) { - public ContentMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } + } - public ContentMovingNotification(IEnumerable> 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 88aa48c7b8b6..5a691c648716 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentMovingToRecycleBinNotification : MovingToRecycleBinNotification { - public sealed class ContentMovingToRecycleBinNotification : MovingToRecycleBinNotification + public ContentMovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base( + target, + messages) { - public ContentMovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } + } - public ContentMovingToRecycleBinNotification(IEnumerable> 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 c009b1cb62a4..a7449f24cc0b 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 69d1751e5832..0400155d3cb1 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 65a8efdadf90..c9a11100899c 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 b9cda7722cc0..f2d18fbba15e 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 a1f370bd9434..50b89e10b80e 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 e12bfa16311b..29b864853cc2 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 6addde88c1b4..d06f364ed2dd 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentSavedBlueprintNotification : ObjectNotification { - public sealed class ContentSavedBlueprintNotification : ObjectNotification + public ContentSavedBlueprintNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentSavedBlueprintNotification(IContent target, EventMessages messages) : base(target, messages) - { - } - - public IContent SavedBlueprint => Target; } + + public IContent SavedBlueprint => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentSavedNotification.cs b/src/Umbraco.Core/Notifications/ContentSavedNotification.cs index b58a366368ec..2d3253117d46 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentSavedNotification : SavedNotification { - public sealed class ContentSavedNotification : SavedNotification + public ContentSavedNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentSavedNotification(IContent target, EventMessages messages) : base(target, messages) - { - } + } - public ContentSavedNotification(IEnumerable 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 afe21bf8708d..4a57a10f29f1 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentSavingNotification : SavingNotification { - public sealed class ContentSavingNotification : SavingNotification + public ContentSavingNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentSavingNotification(IContent target, EventMessages messages) : base(target, messages) - { - } + } - public ContentSavingNotification(IEnumerable 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 0a5c01888304..7d5ee26130c2 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentSendingToPublishNotification : CancelableObjectNotification { - public sealed class ContentSendingToPublishNotification : CancelableObjectNotification + public ContentSendingToPublishNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentSendingToPublishNotification(IContent target, EventMessages messages) : base(target, messages) - { - } - - public IContent Entity => Target; } + + public IContent Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentSentToPublishNotification.cs b/src/Umbraco.Core/Notifications/ContentSentToPublishNotification.cs index c5e2e5dc3b8d..e10b9930e3f5 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentSentToPublishNotification : ObjectNotification { - public sealed class ContentSentToPublishNotification : ObjectNotification + public ContentSentToPublishNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentSentToPublishNotification(IContent target, EventMessages messages) : base(target, messages) - { - } - - public IContent Entity => Target; } + + public IContent Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentSortedNotification.cs b/src/Umbraco.Core/Notifications/ContentSortedNotification.cs index 0a299e3c0a08..8f0d6304ffc2 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 1d6cd31c5ad9..bc3e94a4643f 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 b5b100038b21..df5aab16c733 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 8bd06a4c46f0..d4ced3496d58 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 e03f38131866..606a6fb34e32 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 e0aca73cd2b2..0456ebc9cf97 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 +namespace Umbraco.Cms.Core.Notifications; + +public class ContentTypeChangedNotification : ContentTypeChangeNotification { - public class ContentTypeChangedNotification : ContentTypeChangeNotification + public ContentTypeChangedNotification(ContentTypeChange target, EventMessages messages) + : base( + target, + messages) { - public ContentTypeChangedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } + } - public ContentTypeChangedNotification(IEnumerable> 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 d5b2b3e28e95..92092d1a57ab 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 +namespace Umbraco.Cms.Core.Notifications; + +public class ContentTypeDeletedNotification : DeletedNotification { - public class ContentTypeDeletedNotification : DeletedNotification + public ContentTypeDeletedNotification(IContentType target, EventMessages messages) + : base(target, messages) { - public ContentTypeDeletedNotification(IContentType target, EventMessages messages) : base(target, messages) - { - } + } - public ContentTypeDeletedNotification(IEnumerable 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 56863b93fb13..0313ffcc17d3 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 +namespace Umbraco.Cms.Core.Notifications; + +public class ContentTypeDeletingNotification : DeletingNotification { - public class ContentTypeDeletingNotification : DeletingNotification + public ContentTypeDeletingNotification(IContentType target, EventMessages messages) + : base(target, messages) { - public ContentTypeDeletingNotification(IContentType target, EventMessages messages) : base(target, messages) - { - } + } - public ContentTypeDeletingNotification(IEnumerable 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 d4794329cfc1..4fab7a67ac76 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 +namespace Umbraco.Cms.Core.Notifications; + +public class ContentTypeMovedNotification : MovedNotification { - public class ContentTypeMovedNotification : MovedNotification + public ContentTypeMovedNotification(MoveEventInfo target, EventMessages messages) + : base( + target, + messages) { - public ContentTypeMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } + } - public ContentTypeMovedNotification(IEnumerable> 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 a8881500979a..210dcf43f29e 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 +namespace Umbraco.Cms.Core.Notifications; + +public class ContentTypeMovingNotification : MovingNotification { - public class ContentTypeMovingNotification : MovingNotification + public ContentTypeMovingNotification(MoveEventInfo target, EventMessages messages) + : base( + target, + messages) { - public ContentTypeMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } + } - public ContentTypeMovingNotification(IEnumerable> 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 717225db2d8e..108e72aecc00 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 +namespace Umbraco.Cms.Core.Notifications; + +public abstract class ContentTypeRefreshNotification : ContentTypeChangeNotification + where T : class, IContentTypeComposition { - public abstract class ContentTypeRefreshNotification : ContentTypeChangeNotification where T: class, IContentTypeComposition + protected ContentTypeRefreshNotification(ContentTypeChange target, EventMessages messages) + : base( + target, + messages) { - protected ContentTypeRefreshNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } + } - protected ContentTypeRefreshNotification(IEnumerable> 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 72d111bb6748..b49eef287656 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 +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 { - [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(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } + } - public ContentTypeRefreshedNotification(IEnumerable> 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 5b9a231d60fb..f5c45c632384 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 +namespace Umbraco.Cms.Core.Notifications; + +public class ContentTypeSavedNotification : SavedNotification { - public class ContentTypeSavedNotification : SavedNotification + public ContentTypeSavedNotification(IContentType target, EventMessages messages) + : base(target, messages) { - public ContentTypeSavedNotification(IContentType target, EventMessages messages) : base(target, messages) - { - } + } - public ContentTypeSavedNotification(IEnumerable 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 85deb91418ac..5c1bc5d611e1 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 +namespace Umbraco.Cms.Core.Notifications; + +public class ContentTypeSavingNotification : SavingNotification { - public class ContentTypeSavingNotification : SavingNotification + public ContentTypeSavingNotification(IContentType target, EventMessages messages) + : base(target, messages) { - public ContentTypeSavingNotification(IContentType target, EventMessages messages) : base(target, messages) - { - } + } - public ContentTypeSavingNotification(IEnumerable 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 c08d79ac59e1..2677ef5a08f5 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 5fb5034515b8..7fc0717c0421 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 c7d6c88bcd8a..13b9cf25badf 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; - } + Copy = copy; + ParentId = parentId; + RelateToOriginal = relateToOriginal; + } - public T Original => Target; + public T Original => Target; - public T Copy { get; } + public T Copy { get; } - public int ParentId { get; } - public bool RelateToOriginal { 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 99f46f8b435f..0992f9708b07 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; - } + Copy = copy; + ParentId = parentId; + } - public T Original => Target; + public T Original => Target; - public T Copy { get; } + public T Copy { get; } - public int ParentId { get; } - } + public int ParentId { get; } } diff --git a/src/Umbraco.Core/Notifications/CreatedNotification.cs b/src/Umbraco.Core/Notifications/CreatedNotification.cs index 2108b5fb5cda..8667e4bdcc68 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 +namespace Umbraco.Cms.Core.Notifications; + +public abstract class CreatedNotification : ObjectNotification + where T : class { - public abstract class CreatedNotification : ObjectNotification where T : class + protected CreatedNotification(T target, EventMessages messages) + : base(target, messages) { - protected CreatedNotification(T target, EventMessages messages) : base(target, messages) - { - } - - public T CreatedEntity => Target; } + + public T CreatedEntity => Target; } diff --git a/src/Umbraco.Core/Notifications/CreatingNotification.cs b/src/Umbraco.Core/Notifications/CreatingNotification.cs index da4fbfe742a3..f76a3d883990 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 +namespace Umbraco.Cms.Core.Notifications; + +public abstract class CreatingNotification : CancelableObjectNotification + where T : class { - public abstract class CreatingNotification : CancelableObjectNotification where T : class + protected CreatingNotification(T target, EventMessages messages) + : base(target, messages) { - protected CreatingNotification(T target, EventMessages messages) : base(target, messages) - { - } - - public T CreatedEntity => Target; } + + public T CreatedEntity => Target; } diff --git a/src/Umbraco.Core/Notifications/CreatingRequestNotification.cs b/src/Umbraco.Core/Notifications/CreatingRequestNotification.cs index aacca17afb10..2ea921ceb63c 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 f59de3ebd0ef..5f8b34fb221b 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 405af74c1c67..839fa002302b 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 ab997a0defe0..70035a52376f 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 150582547b17..27065b86191f 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 ae8fb4be6e37..1a54f14622c7 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 6c1a806069d4..ca23336ce1b4 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 +namespace Umbraco.Cms.Core.Notifications; + +public class DataTypeSavedNotification : SavedNotification { - public class DataTypeSavedNotification : SavedNotification + public DataTypeSavedNotification(IDataType target, EventMessages messages) + : base(target, messages) { - public DataTypeSavedNotification(IDataType target, EventMessages messages) : base(target, messages) - { - } + } - public DataTypeSavedNotification(IEnumerable 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 3538950b12f5..8099431da6de 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 +namespace Umbraco.Cms.Core.Notifications; + +public class DataTypeSavingNotification : SavingNotification { - public class DataTypeSavingNotification : SavingNotification + public DataTypeSavingNotification(IDataType target, EventMessages messages) + : base(target, messages) { - public DataTypeSavingNotification(IDataType target, EventMessages messages) : base(target, messages) - { - } + } - public DataTypeSavingNotification(IEnumerable 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 3b2a370388de..69af0581af0b 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 420323afafd6..03b8e150b7dd 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 352eee8caea8..a68593de8053 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; - } + Id = id; + Messages = messages; + SpecificVersion = specificVersion; + DeletePriorVersions = deletePriorVersions; + DateToRetain = dateToRetain; + } - public int Id { get; } + public int Id { get; } - public EventMessages Messages { get; } + public EventMessages Messages { get; } - public int SpecificVersion { get; } + public int SpecificVersion { get; } - public bool DeletePriorVersions { get; } + public bool DeletePriorVersions { get; } - public DateTime DateToRetain { get; } - } + public DateTime DateToRetain { get; } } diff --git a/src/Umbraco.Core/Notifications/DeletingNotification.cs b/src/Umbraco.Core/Notifications/DeletingNotification.cs index b4090a5b8346..ab630468dd7d 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 ca8f1f2514df..6b708da28b35 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 +namespace Umbraco.Cms.Core.Notifications; + +public abstract class DeletingVersionsNotification : DeletedVersionsNotificationBase, ICancelableNotification + where T : class { - 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) { - 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; } } + + public bool Cancel { get; set; } } diff --git a/src/Umbraco.Core/Notifications/DictionaryCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/DictionaryCacheRefresherNotification.cs index b36939800e58..170e8e21be86 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 c151e7ec6002..c62f6d3f7d82 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 5be95c478b1a..d882bb594f6d 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 +namespace Umbraco.Cms.Core.Notifications; + +public class DictionaryItemDeletingNotification : DeletingNotification { - public class DictionaryItemDeletingNotification : DeletingNotification + public DictionaryItemDeletingNotification(IDictionaryItem target, EventMessages messages) + : base(target, messages) { - public DictionaryItemDeletingNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) - { - } + } - public DictionaryItemDeletingNotification(IEnumerable 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 dc5194b847bf..386871a28b7e 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 +namespace Umbraco.Cms.Core.Notifications; + +public class DictionaryItemSavedNotification : SavedNotification { - public class DictionaryItemSavedNotification : SavedNotification + public DictionaryItemSavedNotification(IDictionaryItem target, EventMessages messages) + : base(target, messages) { - public DictionaryItemSavedNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) - { - } + } - public DictionaryItemSavedNotification(IEnumerable 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 79fef15aef96..517fc772a02f 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 +namespace Umbraco.Cms.Core.Notifications; + +public class DictionaryItemSavingNotification : SavingNotification { - public class DictionaryItemSavingNotification : SavingNotification + public DictionaryItemSavingNotification(IDictionaryItem target, EventMessages messages) + : base(target, messages) { - public DictionaryItemSavingNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) - { - } + } - public DictionaryItemSavingNotification(IEnumerable 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 326a7d2b6455..86114b500375 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 b1b3a80ba145..c569afc7b4b9 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 cd678d368990..afeb3fa67c99 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 +namespace Umbraco.Cms.Core.Notifications; + +public class DomainDeletingNotification : DeletingNotification { - public class DomainDeletingNotification : DeletingNotification + public DomainDeletingNotification(IDomain target, EventMessages messages) + : base(target, messages) { - public DomainDeletingNotification(IDomain target, EventMessages messages) : base(target, messages) - { - } + } - public DomainDeletingNotification(IEnumerable 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 61bd8ba3a426..75c93e15b702 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 +namespace Umbraco.Cms.Core.Notifications; + +public class DomainSavedNotification : SavedNotification { - public class DomainSavedNotification : SavedNotification + public DomainSavedNotification(IDomain target, EventMessages messages) + : base(target, messages) { - public DomainSavedNotification(IDomain target, EventMessages messages) : base(target, messages) - { - } + } - public DomainSavedNotification(IEnumerable 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 32a2d71a73b5..673ed92c7205 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 +namespace Umbraco.Cms.Core.Notifications; + +public class DomainSavingNotification : SavingNotification { - public class DomainSavingNotification : SavingNotification + public DomainSavingNotification(IDomain target, EventMessages messages) + : base(target, messages) { - public DomainSavingNotification(IDomain target, EventMessages messages) : base(target, messages) - { - } + } - public DomainSavingNotification(IEnumerable 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 2773f3140fc7..8e648ac14d94 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; - } + DeletedEntities = deletedEntities; + Messages = messages; + } - public IEnumerable DeletedEntities { get; } + public IEnumerable DeletedEntities { get; } - public EventMessages Messages { get; } - } + public EventMessages Messages { get; } } diff --git a/src/Umbraco.Core/Notifications/EmptyingRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/EmptyingRecycleBinNotification.cs index 42005fc9f429..570181941506 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; - } + DeletedEntities = deletedEntities; + Messages = messages; + } - public IEnumerable? DeletedEntities { get; } + public IEnumerable? DeletedEntities { get; } - public EventMessages Messages { get; } + public EventMessages Messages { get; } - public bool Cancel { get; set; } - } + public bool Cancel { get; set; } } diff --git a/src/Umbraco.Core/Notifications/EntityContainerDeletedNotification.cs b/src/Umbraco.Core/Notifications/EntityContainerDeletedNotification.cs index 66c55e94ad8f..5074aa3893c7 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 45a7a5b6c89b..4d22d7715a8b 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 e6046c9a58f2..11e7100b9167 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 c03d5f5ee30b..9e1b795d9f12 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 33cac9effd5d..4fa344683473 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 25cbfc9311ec..6c5455e76287 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 1afc1fa07866..4a5aaa4216c9 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 +namespace Umbraco.Cms.Core.Notifications; + +public class EntityRefreshNotification : ObjectNotification + where T : class, IContentBase { - public class EntityRefreshNotification : ObjectNotification where T : class, IContentBase + public EntityRefreshNotification(T target, EventMessages messages) + : base(target, messages) { - public EntityRefreshNotification(T target, EventMessages messages) : base(target, messages) - { - } - - public T Entity => Target; } + + public T Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/EnumerableObjectNotification.cs b/src/Umbraco.Core/Notifications/EnumerableObjectNotification.cs index fde93f01396e..3989e34b4b97 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 +namespace Umbraco.Cms.Core.Notifications; + +public abstract class EnumerableObjectNotification : ObjectNotification> { - public abstract class EnumerableObjectNotification : ObjectNotification> + protected EnumerableObjectNotification(T target, EventMessages messages) + : base(new[] { target }, messages) { - protected EnumerableObjectNotification(T target, EventMessages messages) : base(new [] {target}, messages) - { - } + } - protected EnumerableObjectNotification(IEnumerable target, EventMessages messages) : base(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 9cf69032e620..29c843945ca9 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; - } + Member = member; + Exported = exported; + } - public IMember Member { get; } + public IMember Member { get; } - public MemberExportModel Exported { get; } - } + public MemberExportModel Exported { get; } } diff --git a/src/Umbraco.Core/Notifications/ICancelableNotification.cs b/src/Umbraco.Core/Notifications/ICancelableNotification.cs index c30e6613feba..e4d1b61309e1 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 2427da1454c6..fc73fba39bfb 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 c7319524ffc3..65603f5bfa76 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 4b0ea6826afb..8d8ea73fe64b 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 8f3538d44819..62114722c1cc 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 +namespace Umbraco.Cms.Core.Notifications; + +public class ImportedPackageNotification : StatefulNotification { - public class ImportedPackageNotification : StatefulNotification - { - public ImportedPackageNotification(InstallationSummary installationSummary) - { - InstallationSummary = installationSummary; - } + public ImportedPackageNotification(InstallationSummary installationSummary) => + InstallationSummary = installationSummary; - public InstallationSummary InstallationSummary { get; } - } + public InstallationSummary InstallationSummary { get; } } diff --git a/src/Umbraco.Core/Notifications/ImportingPackageNotification.cs b/src/Umbraco.Core/Notifications/ImportingPackageNotification.cs index 7fb6c8f9fc81..67a02f254c88 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 436d8a4fbfaa..8e62c68b1d39 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 ccc17c8a902b..9f435775aa92 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 c4e468250022..1fdff6538f0c 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 +namespace Umbraco.Cms.Core.Notifications; + +public class LanguageDeletingNotification : DeletingNotification { - public class LanguageDeletingNotification : DeletingNotification + public LanguageDeletingNotification(ILanguage target, EventMessages messages) + : base(target, messages) { - public LanguageDeletingNotification(ILanguage target, EventMessages messages) : base(target, messages) - { - } + } - public LanguageDeletingNotification(IEnumerable 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 29265c86ca0d..b3e58e9b83c6 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 +namespace Umbraco.Cms.Core.Notifications; + +public class LanguageSavedNotification : SavedNotification { - public class LanguageSavedNotification : SavedNotification + public LanguageSavedNotification(ILanguage target, EventMessages messages) + : base(target, messages) { - public LanguageSavedNotification(ILanguage target, EventMessages messages) : base(target, messages) - { - } + } - public LanguageSavedNotification(IEnumerable 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 5fcb892e2504..adbba95ad484 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 +namespace Umbraco.Cms.Core.Notifications; + +public class LanguageSavingNotification : SavingNotification { - public class LanguageSavingNotification : SavingNotification + public LanguageSavingNotification(ILanguage target, EventMessages messages) + : base(target, messages) { - public LanguageSavingNotification(ILanguage target, EventMessages messages) : base(target, messages) - { - } + } - public LanguageSavingNotification(IEnumerable 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 5fb5554b1b42..4d8815507443 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 237cce38fe3a..b42779415a07 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 d36a9896bc43..8d262cb8aa79 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MacroDeletingNotification : DeletingNotification { - public class MacroDeletingNotification : DeletingNotification + public MacroDeletingNotification(IMacro target, EventMessages messages) + : base(target, messages) { - public MacroDeletingNotification(IMacro target, EventMessages messages) : base(target, messages) - { - } + } - public MacroDeletingNotification(IEnumerable 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 8aa776dcc682..145ac6eb3da2 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MacroSavedNotification : SavedNotification { - public class MacroSavedNotification : SavedNotification + public MacroSavedNotification(IMacro target, EventMessages messages) + : base(target, messages) { - public MacroSavedNotification(IMacro target, EventMessages messages) : base(target, messages) - { - } + } - public MacroSavedNotification(IEnumerable 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 965ee6b22ed7..5786b76d813c 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MacroSavingNotification : SavingNotification { - public class MacroSavingNotification : SavingNotification + public MacroSavingNotification(IMacro target, EventMessages messages) + : base(target, messages) { - public MacroSavingNotification(IMacro target, EventMessages messages) : base(target, messages) - { - } + } - public MacroSavingNotification(IEnumerable 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 079475232d4f..9277e20423b4 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 b8cce7e74761..643f907ab815 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 6bbdb3c09890..b8520e5274f3 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 358a553b286d..8973b9861fa9 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaDeletingNotification : DeletingNotification { - public sealed class MediaDeletingNotification : DeletingNotification + public MediaDeletingNotification(IMedia target, EventMessages messages) + : base(target, messages) { - public MediaDeletingNotification(IMedia target, EventMessages messages) : base(target, messages) - { - } + } - public MediaDeletingNotification(IEnumerable 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 fa7b3ba8e083..0d7ff01ca334 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 086229692546..3aea97d608b2 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 4e257cfb38fb..432d48084796 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 2012f16f4b13..d7cf614ed9ec 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaMovedNotification : MovedNotification { - public sealed class MediaMovedNotification : MovedNotification + public MediaMovedNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) { - public MediaMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } + } - public MediaMovedNotification(IEnumerable> 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 44120674bdc7..78d771847b1c 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaMovedToRecycleBinNotification : MovedToRecycleBinNotification { - public sealed class MediaMovedToRecycleBinNotification : MovedToRecycleBinNotification + public MediaMovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) { - public MediaMovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } + } - public MediaMovedToRecycleBinNotification(IEnumerable> 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 fcfb50787baf..c1f5a7ab942c 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaMovingNotification : MovingNotification { - public sealed class MediaMovingNotification : MovingNotification + public MediaMovingNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) { - public MediaMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } + } - public MediaMovingNotification(IEnumerable> 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 856b66c0c430..ee5618f9fb29 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaMovingToRecycleBinNotification : MovingToRecycleBinNotification { - public sealed class MediaMovingToRecycleBinNotification : MovingToRecycleBinNotification + public MediaMovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) { - public MediaMovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } + } - public MediaMovingToRecycleBinNotification(IEnumerable> 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 1c8b8b9bea65..bd4cb3efdaf3 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 addeda617eb3..bf9f50752181 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaSavedNotification : SavedNotification { - public sealed class MediaSavedNotification : SavedNotification + public MediaSavedNotification(IMedia target, EventMessages messages) + : base(target, messages) { - public MediaSavedNotification(IMedia target, EventMessages messages) : base(target, messages) - { - } + } - public MediaSavedNotification(IEnumerable 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 638d27c96880..d902de6ba74b 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaSavingNotification : SavingNotification { - public sealed class MediaSavingNotification : SavingNotification + public MediaSavingNotification(IMedia target, EventMessages messages) + : base(target, messages) { - public MediaSavingNotification(IMedia target, EventMessages messages) : base(target, messages) - { - } + } - public MediaSavingNotification(IEnumerable 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 00e0e6b42cad..cd896cd1fc55 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 322a6bb1ab09..1882c7cc742c 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MediaTypeChangedNotification : ContentTypeChangeNotification { - public class MediaTypeChangedNotification : ContentTypeChangeNotification + public MediaTypeChangedNotification(ContentTypeChange target, EventMessages messages) + : base(target, messages) { - public MediaTypeChangedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } + } - public MediaTypeChangedNotification(IEnumerable> 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 59c7114ca01e..8ad8e1bce577 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MediaTypeDeletedNotification : DeletedNotification { - public class MediaTypeDeletedNotification : DeletedNotification + public MediaTypeDeletedNotification(IMediaType target, EventMessages messages) + : base(target, messages) { - public MediaTypeDeletedNotification(IMediaType target, EventMessages messages) : base(target, messages) - { - } + } - public MediaTypeDeletedNotification(IEnumerable 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 1cb4f7c99d03..a819ef0d8c02 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MediaTypeDeletingNotification : DeletingNotification { - public class MediaTypeDeletingNotification : DeletingNotification + public MediaTypeDeletingNotification(IMediaType target, EventMessages messages) + : base(target, messages) { - public MediaTypeDeletingNotification(IMediaType target, EventMessages messages) : base(target, messages) - { - } + } - public MediaTypeDeletingNotification(IEnumerable 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 c17aa222ded9..f05d5fd37be5 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MediaTypeMovedNotification : MovedNotification { - public class MediaTypeMovedNotification : MovedNotification + public MediaTypeMovedNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) { - public MediaTypeMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } + } - public MediaTypeMovedNotification(IEnumerable> 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 43499430b0e0..9b7ac27c13ee 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MediaTypeMovingNotification : MovingNotification { - public class MediaTypeMovingNotification : MovingNotification + public MediaTypeMovingNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) { - public MediaTypeMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } + } - public MediaTypeMovingNotification(IEnumerable> 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 6b59e3220ed6..5b6814fdb152 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 +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 { - [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(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } + } - public MediaTypeRefreshedNotification(IEnumerable> 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 b4b2372b7f71..17063f5252c5 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MediaTypeSavedNotification : SavedNotification { - public class MediaTypeSavedNotification : SavedNotification + public MediaTypeSavedNotification(IMediaType target, EventMessages messages) + : base(target, messages) { - public MediaTypeSavedNotification(IMediaType target, EventMessages messages) : base(target, messages) - { - } + } - public MediaTypeSavedNotification(IEnumerable 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 0a93f08671bb..46bc588b39ed 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MediaTypeSavingNotification : SavingNotification { - public class MediaTypeSavingNotification : SavingNotification + public MediaTypeSavingNotification(IMediaType target, EventMessages messages) + : base(target, messages) { - public MediaTypeSavingNotification(IMediaType target, EventMessages messages) : base(target, messages) - { - } + } - public MediaTypeSavingNotification(IEnumerable 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 c2d920843d20..46101878aab6 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 7539d6b13375..b1578fd99812 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 9d09d40e1566..df599d7b0845 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MemberDeletingNotification : DeletingNotification { - public sealed class MemberDeletingNotification : DeletingNotification + public MemberDeletingNotification(IMember target, EventMessages messages) + : base(target, messages) { - public MemberDeletingNotification(IMember target, EventMessages messages) : base(target, messages) - { - } + } - public MemberDeletingNotification(IEnumerable 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 f882b611673c..333a8fbb5542 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 8665cc5f7187..528dc37254ee 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 2b0f94af6426..f0ed3dc49ca2 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MemberGroupDeletingNotification : DeletingNotification { - public class MemberGroupDeletingNotification : DeletingNotification + public MemberGroupDeletingNotification(IMemberGroup target, EventMessages messages) + : base(target, messages) { - public MemberGroupDeletingNotification(IMemberGroup target, EventMessages messages) : base(target, messages) - { - } + } - public MemberGroupDeletingNotification(IEnumerable 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 e5beffe76bac..9f8671d923de 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MemberGroupSavedNotification : SavedNotification { - public class MemberGroupSavedNotification : SavedNotification + public MemberGroupSavedNotification(IMemberGroup target, EventMessages messages) + : base(target, messages) { - public MemberGroupSavedNotification(IMemberGroup target, EventMessages messages) : base(target, messages) - { - } + } - public MemberGroupSavedNotification(IEnumerable 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 a0341ab2ef05..233714c54270 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MemberGroupSavingNotification : SavingNotification { - public class MemberGroupSavingNotification : SavingNotification + public MemberGroupSavingNotification(IMemberGroup target, EventMessages messages) + : base(target, messages) { - public MemberGroupSavingNotification(IMemberGroup target, EventMessages messages) : base(target, messages) - { - } + } - public MemberGroupSavingNotification(IEnumerable 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 a22c48348fac..ddab089c0b18 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 9ea65488339d..446faee2371e 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; - } + MemberIds = memberIds; + Roles = roles; + } - public int[] MemberIds { get; } + public int[] MemberIds { get; } - public string[] Roles { get; } - } + public string[] Roles { get; } } diff --git a/src/Umbraco.Core/Notifications/MemberSavedNotification.cs b/src/Umbraco.Core/Notifications/MemberSavedNotification.cs index 2c4f4755eba6..f59f41f0ecd8 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MemberSavedNotification : SavedNotification { - public sealed class MemberSavedNotification : SavedNotification + public MemberSavedNotification(IMember target, EventMessages messages) + : base(target, messages) { - public MemberSavedNotification(IMember target, EventMessages messages) : base(target, messages) - { - } + } - public MemberSavedNotification(IEnumerable 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 fc8198c6f976..813e6f726921 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MemberSavingNotification : SavingNotification { - public sealed class MemberSavingNotification : SavingNotification + public MemberSavingNotification(IMember target, EventMessages messages) + : base(target, messages) { - public MemberSavingNotification(IMember target, EventMessages messages) : base(target, messages) - { - } + } - public MemberSavingNotification(IEnumerable 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 e06de2624eda..fc9e392598f2 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 c22908c108d3..cbce239394d9 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MemberTypeChangedNotification : ContentTypeChangeNotification { - public class MemberTypeChangedNotification : ContentTypeChangeNotification + public MemberTypeChangedNotification(ContentTypeChange target, EventMessages messages) + : base(target, messages) { - public MemberTypeChangedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } + } - public MemberTypeChangedNotification(IEnumerable> 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 490db24cf360..b3061cc0744c 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MemberTypeDeletedNotification : DeletedNotification { - public class MemberTypeDeletedNotification : DeletedNotification + public MemberTypeDeletedNotification(IMemberType target, EventMessages messages) + : base(target, messages) { - public MemberTypeDeletedNotification(IMemberType target, EventMessages messages) : base(target, messages) - { - } + } - public MemberTypeDeletedNotification(IEnumerable 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 04821eb0c253..d80fcd1c1661 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MemberTypeDeletingNotification : DeletingNotification { - public class MemberTypeDeletingNotification : DeletingNotification + public MemberTypeDeletingNotification(IMemberType target, EventMessages messages) + : base(target, messages) { - public MemberTypeDeletingNotification(IMemberType target, EventMessages messages) : base(target, messages) - { - } + } - public MemberTypeDeletingNotification(IEnumerable 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 8e74076119b6..5ab605612459 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MemberTypeMovedNotification : MovedNotification { - public class MemberTypeMovedNotification : MovedNotification + public MemberTypeMovedNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) { - public MemberTypeMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } + } - public MemberTypeMovedNotification(IEnumerable> 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 b4627aaf30f7..9b4445c17157 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MemberTypeMovingNotification : MovingNotification { - public class MemberTypeMovingNotification : MovingNotification + public MemberTypeMovingNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) { - public MemberTypeMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } + } - public MemberTypeMovingNotification(IEnumerable> 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 89147a523f83..050c24a9e78d 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 +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 { - [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(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } + } - public MemberTypeRefreshedNotification(IEnumerable> 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 768f9e8bb0bc..3101c794e225 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MemberTypeSavedNotification : SavedNotification { - public class MemberTypeSavedNotification : SavedNotification + public MemberTypeSavedNotification(IMemberType target, EventMessages messages) + : base(target, messages) { - public MemberTypeSavedNotification(IMemberType target, EventMessages messages) : base(target, messages) - { - } + } - public MemberTypeSavedNotification(IEnumerable 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 598aadffa4fd..7cfcb12b91c6 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 +namespace Umbraco.Cms.Core.Notifications; + +public class MemberTypeSavingNotification : SavingNotification { - public class MemberTypeSavingNotification : SavingNotification + public MemberTypeSavingNotification(IMemberType target, EventMessages messages) + : base(target, messages) { - public MemberTypeSavingNotification(IMemberType target, EventMessages messages) : base(target, messages) - { - } + } - public MemberTypeSavingNotification(IEnumerable 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 e4adadcd5231..0048699e0984 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; - } + SourceType = sourceType; + ModelType = modelType; + Message = message; + } - /// - /// Gets the type of the source object. - /// - public Type SourceType { get; } + /// + /// Gets the type of the source object. + /// + public Type SourceType { get; } - /// - /// Gets the type of the view model. - /// - public Type ModelType { 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; } - } + /// + /// 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 4573d5e45a72..f67273a6d412 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 1e02d30eb74e..fddb0ab106e8 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 6bf493fc1b32..47a2ecf7bf07 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 ef8c36ce6f13..37e486e3ff65 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 d907d3dcfac2..540cf0840a25 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 a550754d322b..e7c60c5bbc13 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; - } + Messages = messages; + Target = target; + } - public EventMessages Messages { get; } + public EventMessages Messages { get; } - protected T Target { get; } - } + protected T Target { get; } } diff --git a/src/Umbraco.Core/Notifications/PartialViewCreatedNotification.cs b/src/Umbraco.Core/Notifications/PartialViewCreatedNotification.cs index 3f34c4b1c663..3fe571843d53 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 425879fb06b1..d53b4eb1c88d 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 4ef4058b5c3b..29e1548bf326 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 647371340861..26a6fa86e07b 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 +namespace Umbraco.Cms.Core.Notifications; + +public class PartialViewDeletingNotification : DeletingNotification { - public class PartialViewDeletingNotification : DeletingNotification + public PartialViewDeletingNotification(IPartialView target, EventMessages messages) + : base(target, messages) { - public PartialViewDeletingNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } + } - public PartialViewDeletingNotification(IEnumerable 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 d50ed08faf56..e7d0702e021e 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 +namespace Umbraco.Cms.Core.Notifications; + +public class PartialViewSavedNotification : SavedNotification { - public class PartialViewSavedNotification : SavedNotification + public PartialViewSavedNotification(IPartialView target, EventMessages messages) + : base(target, messages) { - public PartialViewSavedNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } + } - public PartialViewSavedNotification(IEnumerable 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 fd2e0ee34a33..ee7401c772a5 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 +namespace Umbraco.Cms.Core.Notifications; + +public class PartialViewSavingNotification : SavingNotification { - public class PartialViewSavingNotification : SavingNotification + public PartialViewSavingNotification(IPartialView target, EventMessages messages) + : base(target, messages) { - public PartialViewSavingNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } + } - public PartialViewSavingNotification(IEnumerable 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 1e753217ab3a..223cf16cc340 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 f6aa16500a18..a90601cf50e7 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 42c4c1bdb976..d135af805b8d 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class PublicAccessEntryDeletingNotification : DeletingNotification { - public sealed class PublicAccessEntryDeletingNotification : DeletingNotification + public PublicAccessEntryDeletingNotification(PublicAccessEntry target, EventMessages messages) + : base(target, messages) { - public PublicAccessEntryDeletingNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) - { - } + } - public PublicAccessEntryDeletingNotification(IEnumerable 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 8c0d253500da..1f92d935d7e7 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class PublicAccessEntrySavedNotification : SavedNotification { - public sealed class PublicAccessEntrySavedNotification : SavedNotification + public PublicAccessEntrySavedNotification(PublicAccessEntry target, EventMessages messages) + : base(target, messages) { - public PublicAccessEntrySavedNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) - { - } + } - public PublicAccessEntrySavedNotification(IEnumerable 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 3fbd666b8dd3..9f9e6f8a4a70 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class PublicAccessEntrySavingNotification : SavingNotification { - public sealed class PublicAccessEntrySavingNotification : SavingNotification + public PublicAccessEntrySavingNotification(PublicAccessEntry target, EventMessages messages) + : base(target, messages) { - public PublicAccessEntrySavingNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) - { - } + } - public PublicAccessEntrySavingNotification(IEnumerable 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 f7af0e9b2993..2d93e077c595 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 +namespace Umbraco.Cms.Core.Notifications; + +public class RelationDeletedNotification : DeletedNotification { - public class RelationDeletedNotification : DeletedNotification + public RelationDeletedNotification(IRelation target, EventMessages messages) + : base(target, messages) { - public RelationDeletedNotification(IRelation target, EventMessages messages) : base(target, messages) - { - } + } - public RelationDeletedNotification(IEnumerable 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 8873d95226f0..54b49afb54d2 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 +namespace Umbraco.Cms.Core.Notifications; + +public class RelationDeletingNotification : DeletingNotification { - public class RelationDeletingNotification : DeletingNotification + public RelationDeletingNotification(IRelation target, EventMessages messages) + : base(target, messages) { - public RelationDeletingNotification(IRelation target, EventMessages messages) : base(target, messages) - { - } + } - public RelationDeletingNotification(IEnumerable 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 8b0313f87cc7..3a0b4d9ec823 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 +namespace Umbraco.Cms.Core.Notifications; + +public class RelationSavedNotification : SavedNotification { - public class RelationSavedNotification : SavedNotification + public RelationSavedNotification(IRelation target, EventMessages messages) + : base(target, messages) { - public RelationSavedNotification(IRelation target, EventMessages messages) : base(target, messages) - { - } + } - public RelationSavedNotification(IEnumerable 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 5afe71da53a0..069e0d5fdc3b 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 +namespace Umbraco.Cms.Core.Notifications; + +public class RelationSavingNotification : SavingNotification { - public class RelationSavingNotification : SavingNotification + public RelationSavingNotification(IRelation target, EventMessages messages) + : base(target, messages) { - public RelationSavingNotification(IRelation target, EventMessages messages) : base(target, messages) - { - } + } - public RelationSavingNotification(IEnumerable 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 ff8cf528916d..1d816a40679e 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 8534edcb49f9..498a4c43706a 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 904a82c08bc4..d9ba61b2b562 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 +namespace Umbraco.Cms.Core.Notifications; + +public class RelationTypeDeletingNotification : DeletingNotification { - public class RelationTypeDeletingNotification : DeletingNotification + public RelationTypeDeletingNotification(IRelationType target, EventMessages messages) + : base(target, messages) { - public RelationTypeDeletingNotification(IRelationType target, EventMessages messages) : base(target, messages) - { - } + } - public RelationTypeDeletingNotification(IEnumerable 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 e2e69475d77a..d0a1aaf16e7e 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 +namespace Umbraco.Cms.Core.Notifications; + +public class RelationTypeSavedNotification : SavedNotification { - public class RelationTypeSavedNotification : SavedNotification + public RelationTypeSavedNotification(IRelationType target, EventMessages messages) + : base(target, messages) { - public RelationTypeSavedNotification(IRelationType target, EventMessages messages) : base(target, messages) - { - } + } - public RelationTypeSavedNotification(IEnumerable 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 2fdebe97e778..e2f7979e869a 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 +namespace Umbraco.Cms.Core.Notifications; + +public class RelationTypeSavingNotification : SavingNotification { - public class RelationTypeSavingNotification : SavingNotification + public RelationTypeSavingNotification(IRelationType target, EventMessages messages) + : base(target, messages) { - public RelationTypeSavingNotification(IRelationType target, EventMessages messages) : base(target, messages) - { - } + } - public RelationTypeSavingNotification(IEnumerable 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 ed76cfbf6957..4ae0a720f72f 100644 --- a/src/Umbraco.Core/Notifications/RemovedMemberRolesNotification.cs +++ b/src/Umbraco.Core/Notifications/RemovedMemberRolesNotification.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class RemovedMemberRolesNotification : MemberRolesNotification { - public class RemovedMemberRolesNotification : MemberRolesNotification + public RemovedMemberRolesNotification(int[] memberIds, string[] roles) + : base(memberIds, roles) { - 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 724069aba76c..ab25fbdeb96e 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 1e4184bc3d7b..4f15827ae4be 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 fded45c6b1aa..280f55538e98 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 +namespace Umbraco.Cms.Core.Notifications; + +public abstract class RolledBackNotification : ObjectNotification + where T : class { - public abstract class RolledBackNotification : ObjectNotification where T : class + protected RolledBackNotification(T target, EventMessages messages) + : base(target, messages) { - protected RolledBackNotification(T target, EventMessages messages) : base(target, messages) - { - } - - public T Entity => Target; } + + public T Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/RollingBackNotification.cs b/src/Umbraco.Core/Notifications/RollingBackNotification.cs index 1064a7897c62..3d06d443ea9d 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 +namespace Umbraco.Cms.Core.Notifications; + +public abstract class RollingBackNotification : CancelableObjectNotification + where T : class { - public abstract class RollingBackNotification : CancelableObjectNotification where T : class + protected RollingBackNotification(T target, EventMessages messages) + : base(target, messages) { - protected RollingBackNotification(T target, EventMessages messages) : base(target, messages) - { - } - - public T Entity => Target; } + + public T Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/RoutingRequestNotification.cs b/src/Umbraco.Core/Notifications/RoutingRequestNotification.cs index c8b2d8e0d68b..b5169aa0abfe 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 f638ec2d3c3b..e0ef991e70c4 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 4d676f68ce44..fd1fa0211301 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 0a9af8c1ff38..655b9b66d10b 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 34962f53963c..9724d4580a7d 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 307ae2103cfe..f72af376c311 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 +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 { - [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 ScopedEntityRemoveNotification(IContentBase target, EventMessages messages) : base(target, messages) - { - } - - public IContentBase Entity => Target; } + + public IContentBase Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/ScriptDeletedNotification.cs b/src/Umbraco.Core/Notifications/ScriptDeletedNotification.cs index 650f2d056451..3ca5f1dc4276 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 085c98d600a6..946dc7f75009 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 +namespace Umbraco.Cms.Core.Notifications; + +public class ScriptDeletingNotification : DeletingNotification { - public class ScriptDeletingNotification : DeletingNotification + public ScriptDeletingNotification(IScript target, EventMessages messages) + : base(target, messages) { - public ScriptDeletingNotification(IScript target, EventMessages messages) : base(target, messages) - { - } + } - public ScriptDeletingNotification(IEnumerable 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 6ccb9f144605..2a292383e9f0 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 +namespace Umbraco.Cms.Core.Notifications; + +public class ScriptSavedNotification : SavedNotification { - public class ScriptSavedNotification : SavedNotification + public ScriptSavedNotification(IScript target, EventMessages messages) + : base(target, messages) { - public ScriptSavedNotification(IScript target, EventMessages messages) : base(target, messages) - { - } + } - public ScriptSavedNotification(IEnumerable 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 92ad0ded4e94..3ab2b13ce45b 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 +namespace Umbraco.Cms.Core.Notifications; + +public class ScriptSavingNotification : SavingNotification { - public class ScriptSavingNotification : SavingNotification + public ScriptSavingNotification(IScript target, EventMessages messages) + : base(target, messages) { - public ScriptSavingNotification(IScript target, EventMessages messages) : base(target, messages) - { - } + } - public ScriptSavingNotification(IEnumerable 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 f87a2a0ba8e4..66d7ee038aa2 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; - } + Message = message; + EmailType = emailType; + } - public NotificationEmailModel Message { get; } + 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; } + /// + /// 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; } - /// - /// 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 07ab3c362680..ff57f9c902bc 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; } + UmbracoContext = umbracoContext; + Children = children; + } - public IEnumerable Children { get; set; } + public IUmbracoContext UmbracoContext { get; } - public SendingAllowedChildrenNotification(IEnumerable children, IUmbracoContext umbracoContext) - { - UmbracoContext = umbracoContext; - Children = children; - } - } + public IEnumerable Children { get; set; } } diff --git a/src/Umbraco.Core/Notifications/SendingContentNotification.cs b/src/Umbraco.Core/Notifications/SendingContentNotification.cs index 4d8d93ce7593..a42fefca68c6 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; } + Content = content; + UmbracoContext = umbracoContext; + } - public ContentItemDisplay Content { get; } + public IUmbracoContext UmbracoContext { get; } - public SendingContentNotification(ContentItemDisplay content, IUmbracoContext umbracoContext) - { - Content = content; - UmbracoContext = umbracoContext; - } - } + public ContentItemDisplay Content { get; } } diff --git a/src/Umbraco.Core/Notifications/SendingDashboardsNotification.cs b/src/Umbraco.Core/Notifications/SendingDashboardsNotification.cs index b81339fcbf39..886e25752940 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; } + Dashboards = dashboards; + UmbracoContext = umbracoContext; + } - public IEnumerable> Dashboards { get; } + public IUmbracoContext UmbracoContext { get; } - public SendingDashboardsNotification(IEnumerable> dashboards, IUmbracoContext umbracoContext) - { - Dashboards = dashboards; - UmbracoContext = umbracoContext; - } - } + public IEnumerable> Dashboards { get; } } diff --git a/src/Umbraco.Core/Notifications/SendingMediaNotification.cs b/src/Umbraco.Core/Notifications/SendingMediaNotification.cs index 2fd8f65a4de4..cca282b3eaaf 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; } + Media = media; + UmbracoContext = umbracoContext; + } - public MediaItemDisplay Media { get; } + public IUmbracoContext UmbracoContext { get; } - public SendingMediaNotification(MediaItemDisplay media, IUmbracoContext umbracoContext) - { - Media = media; - UmbracoContext = umbracoContext; - } - } + public MediaItemDisplay Media { get; } } diff --git a/src/Umbraco.Core/Notifications/SendingMemberNotification.cs b/src/Umbraco.Core/Notifications/SendingMemberNotification.cs index cc868836f919..e9e03a868f53 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; } + Member = member; + UmbracoContext = umbracoContext; + } - public MemberDisplay Member { get; } + public IUmbracoContext UmbracoContext { get; } - public SendingMemberNotification(MemberDisplay member, IUmbracoContext umbracoContext) - { - Member = member; - UmbracoContext = umbracoContext; - } - } + public MemberDisplay Member { get; } } diff --git a/src/Umbraco.Core/Notifications/SendingUserNotification.cs b/src/Umbraco.Core/Notifications/SendingUserNotification.cs index 9e3422f1d9ba..da46ec749e5b 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; } + User = user; + UmbracoContext = umbracoContext; + } - public UserDisplay User { get; } + public IUmbracoContext UmbracoContext { get; } - public SendingUserNotification(UserDisplay user, IUmbracoContext umbracoContext) - { - User = user; - UmbracoContext = umbracoContext; - } - } + public UserDisplay User { get; } } diff --git a/src/Umbraco.Core/Notifications/ServerVariablesParsingNotification.cs b/src/Umbraco.Core/Notifications/ServerVariablesParsingNotification.cs index 7fa83a5a6d94..0171009bf2fb 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 ffc50d6bc966..49910f82238a 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 +namespace Umbraco.Cms.Core.Notifications; + +public abstract class SortedNotification : EnumerableObjectNotification { - public abstract class SortedNotification : EnumerableObjectNotification + protected SortedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) { - protected SortedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable SortedEntities => Target; } + + public IEnumerable SortedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/SortingNotification.cs b/src/Umbraco.Core/Notifications/SortingNotification.cs index 1801bfa65604..26e735f91b65 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 +namespace Umbraco.Cms.Core.Notifications; + +public abstract class SortingNotification : CancelableEnumerableObjectNotification { - public abstract class SortingNotification : CancelableEnumerableObjectNotification + protected SortingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) { - protected SortingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable SortedEntities => Target; } + + public IEnumerable SortedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/StatefulNotification.cs b/src/Umbraco.Core/Notifications/StatefulNotification.cs index 15ee320a40f0..5f84000d48af 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 743cadab6314..4b359d60ec8f 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 8a0c411b1346..868936357782 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 +namespace Umbraco.Cms.Core.Notifications; + +public class StylesheetDeletingNotification : DeletingNotification { - public class StylesheetDeletingNotification : DeletingNotification + public StylesheetDeletingNotification(IStylesheet target, EventMessages messages) + : base(target, messages) { - public StylesheetDeletingNotification(IStylesheet target, EventMessages messages) : base(target, messages) - { - } + } - public StylesheetDeletingNotification(IEnumerable 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 0ceeb209e0a2..2f12bebe15bb 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 +namespace Umbraco.Cms.Core.Notifications; + +public class StylesheetSavedNotification : SavedNotification { - public class StylesheetSavedNotification : SavedNotification + public StylesheetSavedNotification(IStylesheet target, EventMessages messages) + : base(target, messages) { - public StylesheetSavedNotification(IStylesheet target, EventMessages messages) : base(target, messages) - { - } + } - public StylesheetSavedNotification(IEnumerable 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 d08bdebac420..0d6804a76c0c 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 +namespace Umbraco.Cms.Core.Notifications; + +public class StylesheetSavingNotification : SavingNotification { - public class StylesheetSavingNotification : SavingNotification + public StylesheetSavingNotification(IStylesheet target, EventMessages messages) + : base(target, messages) { - public StylesheetSavingNotification(IStylesheet target, EventMessages messages) : base(target, messages) - { - } + } - public StylesheetSavingNotification(IEnumerable 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 689d2a52ffb3..a8b119390f0b 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 01d6dc7e6de1..1bab7d2dc510 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 6434c47c4655..791f43d116c3 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 +namespace Umbraco.Cms.Core.Notifications; + +public class TemplateDeletingNotification : DeletingNotification { - public class TemplateDeletingNotification : DeletingNotification + public TemplateDeletingNotification(ITemplate target, EventMessages messages) + : base(target, messages) { - public TemplateDeletingNotification(ITemplate target, EventMessages messages) : base(target, messages) - { - } + } - public TemplateDeletingNotification(IEnumerable 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 ad75a32c02a6..8b51e795d4bb 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 s_templateForContentTypeKey = "CreateTemplateForContentType"; - private const string s_contentTypeAliasKey = "ContentTypeAlias"; + private const string TemplateForContentTypeKey = "CreateTemplateForContentType"; + private const string ContentTypeAliasKey = "ContentTypeAlias"; - public TemplateSavedNotification(ITemplate target, EventMessages messages) : base(target, messages) - { - } + 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 + public bool CreateTemplateForContentType + { + get { - 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; + return createTemplate; } - set + + return false; + } + + set + { + if (!value is bool && State is not null) { - if (!value is bool && State is not null) - { - State[s_templateForContentTypeKey] = value; - } + State[TemplateForContentTypeKey] = value; } } + } - public string? ContentTypeAlias + public string? ContentTypeAlias + { + get { - get + if (State?.TryGetValue(ContentTypeAliasKey, out var result) ?? false) { - if (State?.TryGetValue(s_contentTypeAliasKey, out var result) ?? false) - { - return result as string; - } - - return null; + return result as string; } - set + return null; + } + + set + { + if (value is not null && State is not null) { - if (value is not null && State is not null) - { - State[s_contentTypeAliasKey] = value; - } + State[ContentTypeAliasKey] = value; } } } diff --git a/src/Umbraco.Core/Notifications/TemplateSavingNotification.cs b/src/Umbraco.Core/Notifications/TemplateSavingNotification.cs index 95a681d2f827..45a325feed22 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 s_templateForContentTypeKey = "CreateTemplateForContentType"; - private const string s_contentTypeAliasKey = "ContentTypeAlias"; + private const string TemplateForContentTypeKey = "CreateTemplateForContentType"; + private const string ContentTypeAliasKey = "ContentTypeAlias"; - public TemplateSavingNotification(ITemplate target, EventMessages messages) : base(target, messages) - { - } + 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) + { + CreateTemplateForContentType = createTemplateForContentType; + ContentTypeAlias = contentTypeAlias; + } - public bool CreateTemplateForContentType + public bool CreateTemplateForContentType + { + get { - 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; + return createTemplate; } - set + + return false; + } + + set + { + if (!value is bool && State is not null) { - if (!value is bool && State is not null) - { - State[s_templateForContentTypeKey] = value; - } + State[TemplateForContentTypeKey] = value; } } + } - public string? ContentTypeAlias + public string? ContentTypeAlias + { + get { - get + if (State?.TryGetValue(ContentTypeAliasKey, out var result) ?? false) { - if (State?.TryGetValue(s_contentTypeAliasKey, out var result) ?? false) - { - return result as string; - } - - return null; + return result as string; } - set + return null; + } + + set + { + if (value is not null && State is not null) { - if (value is not null && State is not null) - { - State[s_contentTypeAliasKey] = value; - } + State[ContentTypeAliasKey] = value; } } } diff --git a/src/Umbraco.Core/Notifications/TreeChangeNotification.cs b/src/Umbraco.Core/Notifications/TreeChangeNotification.cs index bdbd0fc044a3..2187f726597f 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 7f8d8521155a..036d5cf8a42a 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. +// 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 +{ /// - /// Notification that occurs during the Umbraco boot process, before instances of initialize. + /// 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 UmbracoApplicationComponentsInstallingNotification : INotification - { - /// - /// Initializes a new instance of the class. - /// - /// The runtime level - public UmbracoApplicationComponentsInstallingNotification(RuntimeLevel runtimeLevel) => RuntimeLevel = runtimeLevel; + /// The runtime level + public UmbracoApplicationComponentsInstallingNotification(RuntimeLevel runtimeLevel) => RuntimeLevel = runtimeLevel; - /// - /// Gets the runtime level of execution. - /// - public RuntimeLevel RuntimeLevel { get; } - } + /// + /// Gets the runtime level of execution. + /// + public RuntimeLevel RuntimeLevel { get; } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationMainDomAcquiredNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationMainDomAcquiredNotification.cs index 66593ab08654..2bbab6e7eced 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. +// 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 +{ /// - /// 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 196af7dfe1f5..1e3f2b7bfd5e 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 82b87aa3bfc9..7c7e97f29f84 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 - } + // 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; - } + /// + /// 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; } + /// + /// Gets the runtime level. + /// + /// + /// The runtime level. + /// + public RuntimeLevel RuntimeLevel { get; } - /// - public bool IsRestarting { get; } - } + /// + public bool IsRestarting { get; } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs index c6dac40a268c..ce9936a137fb 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 062ca954d94d..a877bd31626c 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 - } + // TODO: Remove this constructor in V10 + } - /// - /// Initializes a new instance of the class. - /// - /// Indicates whether Umbraco is restarting. - public UmbracoApplicationStoppingNotification(bool isRestarting) => IsRestarting = isRestarting; + /// + /// Initializes a new instance of the class. + /// + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStoppingNotification(bool isRestarting) => IsRestarting = isRestarting; - /// - public bool IsRestarting { get; } - } + /// + public bool IsRestarting { get; } } diff --git a/src/Umbraco.Core/Notifications/UmbracoRequestBeginNotification.cs b/src/Umbraco.Core/Notifications/UmbracoRequestBeginNotification.cs index 76683f8d65e3..fedbb6c35b93 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 27fb6ff09d33..a3f977115385 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 7f9b239ce209..c2e4f27b490a 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 4181d74dd765..589a2df68250 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 c272e51b22ad..a5d89bf1674b 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 febfa27d9463..611f8aa0ea22 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class UserDeletingNotification : DeletingNotification { - public sealed class UserDeletingNotification : DeletingNotification + public UserDeletingNotification(IUser target, EventMessages messages) + : base(target, messages) { - public UserDeletingNotification(IUser target, EventMessages messages) : base(target, messages) - { - } + } - public UserDeletingNotification(IEnumerable 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 b4e93f8b6798..b40e902e104b 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 608e5c0f6332..6181a33809bf 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 7aca0d5edb76..d8e519ee9daa 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 9877d9544197..0555611f3a34 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 af0e8d76d6bc..aea73393abce 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class UserGroupDeletingNotification : DeletingNotification { - public sealed class UserGroupDeletingNotification : DeletingNotification + public UserGroupDeletingNotification(IUserGroup target, EventMessages messages) + : base(target, messages) { - public UserGroupDeletingNotification(IUserGroup target, EventMessages messages) : base(target, messages) - { - } + } - public UserGroupDeletingNotification(IEnumerable 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 fee23c06ea8d..aa4484c3d341 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class UserGroupSavedNotification : SavedNotification { - public sealed class UserGroupSavedNotification : SavedNotification + public UserGroupSavedNotification(IUserGroup target, EventMessages messages) + : base(target, messages) { - public UserGroupSavedNotification(IUserGroup target, EventMessages messages) : base(target, messages) - { - } + } - public UserGroupSavedNotification(IEnumerable 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 0dc074bfdcd6..06c82c0298d3 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class UserGroupSavingNotification : SavingNotification { - public sealed class UserGroupSavingNotification : SavingNotification + public UserGroupSavingNotification(IUserGroup target, EventMessages messages) + : base(target, messages) { - public UserGroupSavingNotification(IUserGroup target, EventMessages messages) : base(target, messages) - { - } + } - public UserGroupSavingNotification(IEnumerable 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 5e239660aa61..399d1946908f 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class UserGroupWithUsersSavedNotification : SavedNotification { - public sealed class UserGroupWithUsersSavedNotification : SavedNotification + public UserGroupWithUsersSavedNotification(UserGroupWithUsers target, EventMessages messages) + : base(target, messages) { - public UserGroupWithUsersSavedNotification(UserGroupWithUsers target, EventMessages messages) : base(target, messages) - { - } + } - public UserGroupWithUsersSavedNotification(IEnumerable 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 f3dd362c20f1..c34d66841c1a 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class UserGroupWithUsersSavingNotification : SavingNotification { - public sealed class UserGroupWithUsersSavingNotification : SavingNotification + public UserGroupWithUsersSavingNotification(UserGroupWithUsers target, EventMessages messages) + : base( + target, + messages) { - public UserGroupWithUsersSavingNotification(UserGroupWithUsers target, EventMessages messages) : base(target, messages) - { - } + } - public UserGroupWithUsersSavingNotification(IEnumerable 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 b7485d98524d..81fc798f639f 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 ff07b57832d9..a8cb3e9cc448 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 5a975a19515b..57f037712c69 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 e9b79c68fecf..5b20ca48ef86 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 92e7dea03f6b..c93d42accf97 100644 --- a/src/Umbraco.Core/Notifications/UserLogoutSuccessNotification.cs +++ b/src/Umbraco.Core/Notifications/UserLogoutSuccessNotification.cs @@ -1,11 +1,11 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserLogoutSuccessNotification : UserNotification { - public class UserLogoutSuccessNotification : UserNotification + public UserLogoutSuccessNotification(string ipAddress, string? affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserLogoutSuccessNotification(string ipAddress, string? affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } - - public string? SignOutRedirectUrl { get; set; } } + + public string? SignOutRedirectUrl { get; set; } } diff --git a/src/Umbraco.Core/Notifications/UserNotification.cs b/src/Umbraco.Core/Notifications/UserNotification.cs index f0ce83c8fb05..6141cdf389e0 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; - } + DateTimeUtc = DateTime.UtcNow; + IpAddress = ipAddress; + AffectedUserId = affectedUserId; + PerformingUserId = performingUserId; + } - /// - /// Current date/time in UTC format - /// - public DateTime DateTimeUtc { get; } + /// + /// 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 source IP address of the user performing the action + /// + public string IpAddress { get; } - /// - /// The user affected by the event raised - /// - public string? AffectedUserId { 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; } - } + /// + /// 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 098be36867d9..a7cd1e51aebc 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 fc60eef61e21..8b23b5aa4f05 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 5cd03cc140da..f1cce2df63a0 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 892218af821d..8292cb9f6d19 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class UserSavedNotification : SavedNotification { - public sealed class UserSavedNotification : SavedNotification + public UserSavedNotification(IUser target, EventMessages messages) + : base(target, messages) { - public UserSavedNotification(IUser target, EventMessages messages) : base(target, messages) - { - } + } - public UserSavedNotification(IEnumerable 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 57c0d867fa97..3760f02881ac 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 +namespace Umbraco.Cms.Core.Notifications; + +public sealed class UserSavingNotification : SavingNotification { - public sealed class UserSavingNotification : SavingNotification + public UserSavingNotification(IUser target, EventMessages messages) + : base(target, messages) { - public UserSavingNotification(IUser target, EventMessages messages) : base(target, messages) - { - } + } - public UserSavingNotification(IEnumerable 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 ccb07c593ca5..1eb6d774d0ce 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 0c6cc7b9fdea..7883595733c6 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 16cd4ad0a426..fdb76f4bc2ee 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; + private readonly ConflictingPackageData _conflictingPackageData; - public CompiledPackageXmlParser(ConflictingPackageData conflictingPackageData) => _conflictingPackageData = conflictingPackageData; + public CompiledPackageXmlParser(ConflictingPackageData conflictingPackageData) => + _conflictingPackageData = conflictingPackageData; - public CompiledPackage ToCompiledPackage(XDocument xml) + 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"); + throw new ArgumentNullException(nameof(xml)); + } - 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"); + if (xml.Root == null) + { + throw new InvalidOperationException("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(), - }; + if (xml.Root.Name != "umbPackage") + { + throw new FormatException("The xml document is invalid"); + } - def.Warnings = GetInstallWarnings(def); + XElement? info = xml.Root.Element("info"); + if (info == null) + { + throw new FormatException("The xml document is invalid"); + } - return def; + XElement? package = info.Element("package"); + if (package == null) + { + throw new FormatException("The xml document is invalid"); } - private InstallWarnings GetInstallWarnings(CompiledPackage package) + var def = new CompiledPackage { - var installWarnings = new InstallWarnings - { - ConflictingMacros = _conflictingPackageData.FindConflictingMacros(package.Macros), - ConflictingTemplates = _conflictingPackageData.FindConflictingTemplates(package.Templates), - ConflictingStylesheets = _conflictingPackageData.FindConflictingStylesheets(package.Stylesheets) - }; + // 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(), + }; - return installWarnings; - } + 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 239f1ba66d47..d71eada6184a 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 IMacroService _macroService; - private readonly IFileService _fileService; + private readonly IFileService _fileService; + private readonly IMacroService _macroService; - public ConflictingPackageData(IMacroService macroService, IFileService fileService) - { - _fileService = fileService ?? throw new ArgumentNullException(nameof(fileService)); - _macroService = macroService ?? throw new ArgumentNullException(nameof(macroService)); - } + 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 => + public IEnumerable? FindConflictingStylesheets(IEnumerable? stylesheetNodes) => + stylesheetNodes? + .Select(n => + { + XElement? xElement = n.Element("Name") ?? n.Element("name"); + if (xElement == null) { - 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 => + 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) { - 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 => + 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) { - var xElement = n.Element("alias") ?? n.Element("Alias"); - if (xElement == null) - throw new FormatException("missing a \"alias\" element in alias element"); + throw new FormatException("missing a \"alias\" element in alias element"); + } - return _macroService.GetByAlias(xElement.Value); - }) - .Where(v => v != null); - } - } + 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 ba99fdd9a7a0..3c873eb90808 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 fe015006a82b..b66f4884afe8 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 { + IEnumerable GetAll(); + + PackageDefinition? GetById(int id); + + void Delete(int id); + /// - /// Defines methods for persisting package definitions to storage + /// Persists a package definition to storage /// - public interface IPackageDefinitionRepository - { - IEnumerable GetAll(); - 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); - } + /// + /// 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 9a744a91faf1..7fc714bfdbb8 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 +namespace Umbraco.Cms.Core.Packaging; + +public interface IPackageInstallation { - 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); + /// + /// 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); - } + /// + /// 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 d5d7ad343be2..d72ede149469 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 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() - { - var sb = new StringBuilder(); + public InstallationSummary(string packageName) + => PackageName = packageName; - 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)); + public string PackageName { get; } - if (appendLine) - { - sb.AppendLine(); - } - } - } + 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(); - void WriteCount(string message, IEnumerable source, bool appendLine = true) + 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() + { + 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(source?.Count() ?? 0); + sb.Append(string.Join(", ", result)); if (appendLine) { sb.AppendLine(); } } + } + + void WriteCount(string message, IEnumerable source, bool appendLine = true) + { + sb.Append(message); + sb.Append(source?.Count() ?? 0); - 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(); + 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 ded901512b9e..3f3cc24a2ad9 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 -{ - [DataContract(Name = "installedPackage")] - public class InstalledPackage - { - [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 +namespace Umbraco.Cms.Core.Packaging; - [DataMember(Name = "packageView")] - public string? PackageView { get; set; } +[DataContract(Name = "installedPackage")] +public class InstalledPackage +{ + [DataMember(Name = "name", IsRequired = true)] + [Required] + public string? PackageName { get; set; } - [DataMember(Name = "plans")] - public IEnumerable PackageMigrationPlans { get; set; } = Enumerable.Empty(); + // TODO: Version? Icon? Other metadata? This would need to come from querying the package on Our + [DataMember(Name = "packageView")] + public string? PackageView { get; set; } - /// - /// It the package contains any migrations at all - /// - [DataMember(Name = "hasMigrations")] - public bool HasMigrations => PackageMigrationPlans.Any(); + [DataMember(Name = "plans")] + public IEnumerable PackageMigrationPlans { get; set; } = + Enumerable.Empty(); - /// - /// 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 5aaca2e9f2cb..50cafd1d208f 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 -{ - [DataContract(Name = "installedPackageMigrations")] - public class InstalledPackageMigrationPlans - { - [DataMember(Name = "hasPendingMigrations")] - public bool HasPendingMigrations => FinalMigrationId != CurrentMigrationId; +namespace Umbraco.Cms.Core.Packaging; - /// - /// 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; } +[DataContract(Name = "installedPackageMigrations")] +public class InstalledPackageMigrationPlans +{ + [DataMember(Name = "hasPendingMigrations")] + public bool HasPendingMigrations => FinalMigrationId != CurrentMigrationId; - /// - /// 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 66a0a9e102aa..7b0b5f5df491 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; } +/// +/// 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 = "packageGuid")] + public Guid PackageId { get; set; } - [DataMember(Name = "name")] - [Required] - public string Name { get; set; } = string.Empty; + [DataMember(Name = "name")] + [Required] + public string Name { get; set; } = string.Empty; - /// - /// The full path to the package's XML file. - /// - [ReadOnly(true)] - [DataMember(Name = "packagePath")] - public string PackagePath { get; set; } = string.Empty; + /// + /// The full path to the package's XML file. + /// + [ReadOnly(true)] + [DataMember(Name = "packagePath")] + public string PackagePath { get; set; } = string.Empty; - [DataMember(Name = "contentLoadChildNodes")] - public bool ContentLoadChildNodes { get; set; } + [DataMember(Name = "contentLoadChildNodes")] + public bool ContentLoadChildNodes { get; set; } - [DataMember(Name = "contentNodeId")] - public string? ContentNodeId { get; set; } + [DataMember(Name = "contentNodeId")] + public string? ContentNodeId { get; set; } - [DataMember(Name = "macros")] - public IList Macros { get; set; } = new List(); + [DataMember(Name = "macros")] + public IList Macros { get; set; } = new List(); - [DataMember(Name = "languages")] - public IList Languages { get; set; } = new List(); + [DataMember(Name = "languages")] + public IList Languages { get; set; } = new List(); - [DataMember(Name = "dictionaryItems")] - public IList DictionaryItems { get; set; } = new List(); + [DataMember(Name = "dictionaryItems")] + public IList DictionaryItems { get; set; } = new List(); - [DataMember(Name = "templates")] - public IList Templates { get; set; } = new List(); + [DataMember(Name = "templates")] + public IList Templates { get; set; } = new List(); - [DataMember(Name = "partialViews")] - public IList PartialViews { get; set; } = new List(); + [DataMember(Name = "partialViews")] + public IList PartialViews { get; set; } = new List(); - [DataMember(Name = "documentTypes")] - public IList DocumentTypes { get; set; } = new List(); + [DataMember(Name = "documentTypes")] + public IList DocumentTypes { get; set; } = new List(); - [DataMember(Name = "mediaTypes")] - public IList MediaTypes { get; set; } = new List(); + [DataMember(Name = "mediaTypes")] + public IList MediaTypes { get; set; } = new List(); - [DataMember(Name = "stylesheets")] - public IList Stylesheets { get; set; } = new List(); + [DataMember(Name = "stylesheets")] + public IList Stylesheets { get; set; } = new List(); - [DataMember(Name = "scripts")] - public IList Scripts { 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 = "dataTypes")] + public IList DataTypes { get; set; } = new List(); - [DataMember(Name = "mediaUdis")] - public IList MediaUdis { 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 df5375ad9231..99a18dbcf9fb 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 -{ - /// - /// Converts a to and from XML - /// - public class PackageDefinitionXmlParser - { - private static readonly IList s_emptyStringList = new List(); - private static readonly IList s_emptyGuidUdiList = new List(); +namespace Umbraco.Cms.Core.Packaging; +/// +/// 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) + 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 b972a2cf0881..0d72cad38ac8 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; + zipArchive = GetPackageDataManifest(zipStream, out packageXml); + return packageXml; } - public static XDocument? GetEmbeddedPackageDataManifest(Type planType, out ZipArchive? zipArchive) + 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) { - XDocument? packageXml; - var zipStream = GetEmbeddedPackageZipStream(planType); - if (zipStream is not null) - { - zipArchive = GetPackageDataManifest(zipStream, out packageXml); - return packageXml; - } - - zipArchive = null; - packageXml = GetEmbeddedPackageXmlDoc(planType); - return packageXml; + return stream.GetStreamHash(); } - public static XDocument? GetEmbeddedPackageDataManifest(Type planType) + XDocument? xml = GetEmbeddedPackageXmlDoc(planType); + + if (xml is not null) { - return GetEmbeddedPackageDataManifest(planType, out _); + return xml.ToString(); } - private static XDocument? GetEmbeddedPackageXmlDoc(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) { - // 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 null; } - public static string GetEmbeddedPackageDataManifestHash(Type planType) + XDocument xml; + using (stream) { - // 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); + xml = XDocument.Load(stream); + } - if (stream is not null) - { - return stream.GetStreamHash(); - } + return xml; + } - var xml = GetEmbeddedPackageXmlDoc(planType); + public static bool TryGetEmbeddedPackageDataManifest(Type planType, out XDocument? packageXml, out ZipArchive? zipArchive) + { + Stream? zipStream = GetEmbeddedPackageZipStream(planType); + if (zipStream is not null) + { + zipArchive = GetPackageDataManifest(zipStream, out packageXml); + return true; + } - if (xml is not null) - { - return xml.ToString(); - } + zipArchive = null; + packageXml = GetEmbeddedPackageXmlDoc(planType); + return packageXml is not null; + } - throw new IOException("Missing embedded files for planType: " + planType); + public static ZipArchive GetPackageDataManifest(Stream packageZipStream, out XDocument packageXml) + { + if (packageZipStream == null) + { + throw new ArgumentNullException(nameof(packageZipStream)); } - public static bool TryGetEmbeddedPackageDataManifest(Type planType, out XDocument? packageXml, out ZipArchive? zipArchive) + var zip = new ZipArchive(packageZipStream, ZipArchiveMode.Read); + ZipArchiveEntry? packageXmlEntry = zip.GetEntry("package.xml"); + if (packageXmlEntry == null) { - 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; + throw new InvalidOperationException("Zip package does not contain the required package.xml file"); } - public static ZipArchive GetPackageDataManifest(Stream packageZipStream, out XDocument packageXml) + using (Stream packageXmlStream = packageXmlEntry.Open()) + using (var xmlReader = XmlReader.Create(packageXmlStream, new XmlReaderSettings { IgnoreWhitespace = true })) { - 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; + packageXml = XDocument.Load(xmlReader); } + + return zip; } } diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index 174faa37fd8b..a5982aef7e48 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.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)) - 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; - } - - private string CreatedPackagesFile => _packagesFolderPath.EnsureEndsWith('/') + _packageRepositoryFileName; - - public IEnumerable GetAll() - { - var packagesXml = EnsureStorage(out _); - if (packagesXml?.Root == null) - yield break; - - foreach (var packageXml in packagesXml.Root.Elements("package")) - yield return _parser.ToPackageDefinition(packageXml); - } - - public PackageDefinition? GetById(int id) - { - 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); - } - - public void Delete(int id) - { - var packagesXml = EnsureStorage(out var packagesFile); - var packageXml = packagesXml?.Root?.Elements("package").FirstOrDefault(x => x.AttributeValue("id") == id); - if (packageXml == null) - return; + 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; + } - packageXml.Remove(); + private string CreatedPackagesFile => _packagesFolderPath.EnsureEndsWith('/') + _packageRepositoryFileName; - packagesXml?.Save(packagesFile); + public IEnumerable GetAll() + { + XDocument packagesXml = EnsureStorage(out _); + if (packagesXml?.Root == null) + { + yield break; } - public bool SavePackage(PackageDefinition definition) + foreach (XElement packageXml in packagesXml.Root.Elements("package")) { - if (definition == null) - throw new ArgumentNullException(nameof(definition)); + yield return _parser.ToPackageDefinition(packageXml); + } + } - var packagesXml = EnsureStorage(out var packagesFile); + 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); + } - if (packagesXml?.Root == null) - return false; + 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; + } + + packageXml.Remove(); + + packagesXml?.Save(packagesFile); + } + + public bool SavePackage(PackageDefinition definition) + { + if (definition == null) + { + throw new ArgumentNullException(nameof(definition)); + } - //ensure it's valid - ValidatePackage(definition); + XDocument packagesXml = EnsureStorage(out var packagesFile); - if (definition.Id == default) + 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) { - //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); + return false; } - else - { - //existing - var packageXml = packagesXml.Root.Elements("package").FirstOrDefault(x => x.AttributeValue("id") == definition.Id); - if (packageXml == null) - return false; - var updatedXml = _parser.ToXml(definition); - packageXml.ReplaceWith(updatedXml); - } + XElement updatedXml = _parser.ToXml(definition); + packageXml.ReplaceWith(updatedXml); + } - packagesXml.Save(packagesFile); + packagesXml.Save(packagesFile); + + return true; + } - return true; + public string ExportPackage(PackageDefinition definition) + { + if (definition.Id == default) + { + throw new ArgumentException( + "The package definition does not have an ID, it must be saved before being exported"); } - public string ExportPackage(PackageDefinition definition) + if (definition.PackageId == default) { - 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"); + throw new ArgumentException( + "the package definition does not have a GUID, it must be saved before being exported"); + } - //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); - } + // Create a folder for building this package + var temporaryPath = + _hostingEnvironment.MapPathContentRoot(_tempFolderPath.EnsureEndsWith('/') + Guid.NewGuid()); + if (Directory.Exists(temporaryPath) == false) + { + Directory.CreateDirectory(temporaryPath); + } - try + 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) { - //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) + fileName = "package.zip"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) { - 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()) { - 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); - } - } + compiledPackageXml.Save(entryStream); } - } - else - { - fileName = "package.xml"; - tempPackagePath = Path.Combine(temporaryPath, fileName); - using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + foreach (KeyValuePair mediaFile in mediaFiles) { - compiledPackageXml.Save(fileStream); + 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 + { + fileName = "package.xml"; + tempPackagePath = Path.Combine(temporaryPath, fileName); - var directoryName = _hostingEnvironment.MapPathContentRoot(Path.Combine(_createdPackagesFolderPath, definition.Name.Replace(' ', '_'))); - Directory.CreateDirectory(directoryName); - - var finalPackagePath = Path.Combine(directoryName, fileName); - - if (File.Exists(finalPackagePath)) + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) { - File.Delete(finalPackagePath); + compiledPackageXml.Save(fileStream); } + } - File.Move(tempPackagePath, finalPackagePath); + var directoryName = + _hostingEnvironment.MapPathContentRoot(Path.Combine( + _createdPackagesFolderPath, + definition.Name.Replace(' ', '_'))); + Directory.CreateDirectory(directoryName); - definition.PackagePath = finalPackagePath; - SavePackage(definition); + var finalPackagePath = Path.Combine(directoryName, fileName); - return finalPackagePath; - } - finally + if (File.Exists(finalPackagePath)) { - // Clean up - Directory.Delete(temporaryPath, true); + File.Delete(finalPackagePath); } - } - 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))); - } + File.Move(tempPackagePath, finalPackagePath); + + definition.PackagePath = finalPackagePath; + SavePackage(definition); - private void PackageDataTypes(PackageDefinition definition, XContainer root) + return finalPackagePath; + } + finally { - 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); + // Clean up + Directory.Delete(temporaryPath, true); } + } - private void PackageLanguages(PackageDefinition definition, XContainer root) + public void DeleteLocalRepositoryFiles() + { + var packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); + if (File.Exists(packagesFile)) { - 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); + File.Delete(packagesFile); } - private void PackageDictionaryItems(PackageDefinition definition, XContainer root) + var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); + if (Directory.Exists(packagesFolder)) { - 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; - } + Directory.Delete(packagesFolder); + } + } - IDictionaryItem? di = _languageService.GetDictionaryItemById(outInt); + private static XElement GetPackageInfoXml(PackageDefinition definition) + { + var info = new XElement("info"); - if (di == null) - { - continue; - } + // Package info + var package = new XElement("package"); + package.Add(new XElement("name", definition.Name)); + info.Add(package); + return info; + } - items[di.Key] = (di, _serializer.Serialize(di, false)); - } + 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))); + } + } - // organize them in hierarchy ... - var itemCount = items.Count; - var processed = new Dictionary(); - while (processed.Count < itemCount) + 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)) { - foreach (Guid key in items.Keys.ToList()) - { - (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; - - if (!dictionaryItem.ParentId.HasValue) - { - // if it has no parent, its definitely just at the root - AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); - } - 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); - } - } - } + continue; } - root.Add(rootDictionaryItems); - - static void AppendDictionaryElement(XElement rootDictionaryItems, Dictionary items, Dictionary processed, Guid key, XElement serializedDictionaryValue) + IDataType? dataType = _dataTypeService.GetDataType(outInt); + if (dataType == null) { - // track it - processed.Add(key, serializedDictionaryValue); - // append it - rootDictionaryItems.Add(serializedDictionaryValue); - // remove it so its not re-processed - items.Remove(key); + continue; } + + dataTypes.Add(_serializer.Serialize(dataType)); } - private void PackageMacros(PackageDefinition definition, XContainer root) + root.Add(dataTypes); + } + + private void PackageLanguages(PackageDefinition definition, XContainer root) + { + var languages = new XElement("Languages"); + foreach (var langId in definition.Languages) { - var packagedMacros = new List(); - var macros = new XElement("Macros"); - foreach (var macroId in definition.Macros) + if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - 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); + ILanguage? lang = _languageService.GetLanguageById(outInt); + if (lang == null) + { + continue; + } - // 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); + languages.Add(_serializer.Serialize(lang)); } - private void PackageStylesheets(PackageDefinition definition, XContainer root) + 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) { - var stylesheetsXml = new XElement("Stylesheets"); - foreach (var stylesheet in definition.Stylesheets) + if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - if (stylesheet.IsNullOrWhiteSpace()) - { - continue; - } + continue; + } - XElement? xml = GetStylesheetXml(stylesheet, true); - if (xml is not null) - { - stylesheetsXml.Add(xml); - } + IDictionaryItem? di = _languageService.GetDictionaryItemById(outInt); + + if (di == null) + { + continue; } - root.Add(stylesheetsXml); + + items[di.Key] = (di, _serializer.Serialize(di, false)); } - private void PackageStaticFiles( - IEnumerable filePaths, - XContainer root, - string containerName, - string elementName, - IFileSystem? fileSystem) + // organize them in hierarchy ... + var itemCount = items.Count; + var processed = new Dictionary(); + while (processed.Count < itemCount) { - var scriptsXml = new XElement(containerName); - foreach (var file in filePaths) + foreach (Guid key in items.Keys.ToList()) { - if (file.IsNullOrWhiteSpace()) - { - continue; - } + (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; - if (!fileSystem?.FileExists(file) ?? false) + if (!dictionaryItem.ParentId.HasValue) { - throw new InvalidOperationException("No file found with path " + file); + // if it has no parent, its definitely just at the root + AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); } - - using (Stream stream = fileSystem!.OpenFile(file)) + else { - using (var reader = new StreamReader(stream)) + if (processed.ContainsKey(dictionaryItem.ParentId.Value)) { - var fileContents = reader.ReadToEnd(); - scriptsXml.Add( - new XElement( - elementName, - new XAttribute("path", file), - new XCData(fileContents))); + // 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 + } + 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); } } } + } - root.Add(scriptsXml); + root.Add(rootDictionaryItems); + + 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 PackageTemplates(PackageDefinition definition, XContainer root) + private void PackageMacros(PackageDefinition definition, XContainer root) + { + var packagedMacros = new List(); + var macros = new XElement("Macros"); + foreach (var macroId in definition.Macros) { - var templatesXml = new XElement("Templates"); - foreach (var templateId in definition.Templates) + if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - 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)); + continue; } - 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) + XElement? macroXml = GetMacroXml(outInt, out IMacro? macro); + if (macroXml == null) { - if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - continue; - var contentType = _contentTypeService.Get(outInt); - if (contentType == null) - continue; - AddDocumentType(contentType, contentTypes); + continue; } - foreach (var contentType in contentTypes) - docTypesXml.Add(_serializer.Serialize(contentType)); - root.Add(docTypesXml); + macros.Add(macroXml); + if (macro is not null) + { + packagedMacros.Add(macro); + } } - private void PackageMediaTypes(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 mediaTypes = new HashSet(); - var mediaTypesXml = new XElement("MediaTypes"); - foreach (var mediaTypeId in definition.MediaTypes) + if (stylesheet.IsNullOrWhiteSpace()) { - if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - continue; - var mediaType = _mediaTypeService.Get(outInt); - if (mediaType == null) - continue; - AddMediaType(mediaType, mediaTypes); + continue; } - foreach (var mediaType in mediaTypes) - mediaTypesXml.Add(_serializer.Serialize(mediaType)); - root.Add(mediaTypesXml); + XElement? xml = GetStylesheetXml(stylesheet, true); + if (xml is not null) + { + stylesheetsXml.Add(xml); + } } - private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root) + root.Add(stylesheetsXml); + } + + private void PackageStaticFiles( + IEnumerable filePaths, + XContainer root, + string containerName, + string elementName, + IFileSystem? fileSystem) + { + var scriptsXml = new XElement(containerName); + foreach (var file in filePaths) { - //Documents and tags - if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse(definition.ContentNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentNodeId)) + if (file.IsNullOrWhiteSpace()) + { + continue; + } + + if (!fileSystem?.FileExists(file) ?? false) + { + throw new InvalidOperationException("No file found with path " + file); + } + + using (Stream stream = fileSystem!.OpenFile(file)) { - if (contentNodeId > 0) + using (var reader = new StreamReader(stream)) { - //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); - } + var fileContents = reader.ReadToEnd(); + scriptsXml.Add( + new XElement( + elementName, + new XAttribute("path", file), + new XCData(fileContents))); } } } + root.Add(scriptsXml); + } - private Dictionary PackageMedia(PackageDefinition definition, XElement root) + private void PackageTemplates(PackageDefinition definition, XContainer root) + { + var templatesXml = new XElement("Templates"); + foreach (var templateId in definition.Templates) { - var mediaStreams = new Dictionary(); - - // callback that occurs on each serialized media item - void OnSerializedMedia(IMedia media, XElement xmlMedia) + if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - // 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)); + continue; + } - // add the stream to our outgoing stream - mediaStreams.Add(mediaFilePath, mediaStream); - } + ITemplate? template = _fileService.GetTemplate(outInt); + if (template == null) + { + continue; } - IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); + templatesXml.Add(_serializer.Serialize(template)); + } - var mediaXml = new XElement( - "MediaItems", - medias.Select(media => - { - XElement serializedMedia = _serializer.Serialize( - media, - definition.MediaLoadChildNodes, - OnSerializedMedia); + root.Add(templatesXml); + } - return new XElement("MediaSet", serializedMedia); - })); + 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; + } - root.Add(mediaXml); + IContentType? contentType = _contentTypeService.Get(outInt); + if (contentType == null) + { + continue; + } - return mediaStreams; + AddDocumentType(contentType, contentTypes); } - // TODO: Delete this - /// - private XElement? GetMacroXml(int macroId, out IMacro? macro) + foreach (IContentType contentType in contentTypes) { - macro = _macroService.GetById(macroId); - if (macro == null) - return null; - var xml = _serializer.Serialize(macro); - return xml; + docTypesXml.Add(_serializer.Serialize(contentType)); } - /// - /// 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) + 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 (string.IsNullOrWhiteSpace(path)) + if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + continue; } - IStylesheet? stylesheet = _fileService.GetStylesheet(path); - if (stylesheet == null) + IMediaType? mediaType = _mediaTypeService.Get(outInt); + if (mediaType == null) { - return null; + continue; } - return _serializer.Serialize(stylesheet, includeProperties); + AddMediaType(mediaType, mediaTypes); + } + + foreach (IMediaType mediaType in mediaTypes) + { + mediaTypesXml.Add(_serializer.Serialize(mediaType)); } - private void AddDocumentType(IContentType dt, HashSet dtl) + 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 (dt.ParentId > 0) + if (contentNodeId > 0) { - var parent = _contentTypeService.Get(dt.ParentId); - if (parent != null) // could be a container - AddDocumentType(parent, dtl); + // load content from umbraco. + IContent? content = _contentService.GetById(contentNodeId); + if (content != null) + { + XElement 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); + } } - - if (!dtl.Contains(dt)) - dtl.Add(dt); } + } + + private Dictionary PackageMedia(PackageDefinition definition, XElement root) + { + var mediaStreams = new Dictionary(); - private void AddMediaType(IMediaType mediaType, HashSet mediaTypes) + // callback that occurs on each serialized media item + void OnSerializedMedia(IMedia media, XElement xmlMedia) { - if (mediaType.ParentId > 0) + // 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) { - var parent = _mediaTypeService.Get(mediaType.ParentId); - if (parent != null) // could be a container - AddMediaType(parent, mediaTypes); - } + xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath)); - if (!mediaTypes.Contains(mediaType)) - mediaTypes.Add(mediaType); + // add the stream to our outgoing stream + mediaStreams.Add(mediaFilePath, mediaStream); + } } - private static XElement GetPackageInfoXml(PackageDefinition definition) + 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) { - var info = new XElement("info"); + return null; + } - //Package info - var package = new XElement("package"); - package.Add(new XElement("name", definition.Name)); - info.Add(package); - return info; + 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)); } - private static XDocument CreateCompiledPackageXml(out XElement root) + IStylesheet? stylesheet = _fileService.GetStylesheet(path); + if (stylesheet == null) { - root = new XElement("umbPackage"); - var compiledPackageXml = new XDocument(root); - return compiledPackageXml; + return null; } - private XDocument EnsureStorage(out string packagesFile) + return _serializer.Serialize(stylesheet, includeProperties); + } + + private void AddDocumentType(IContentType dt, HashSet dtl) + { + if (dt.ParentId > 0) { - var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); - Directory.CreateDirectory(packagesFolder); + IContentType? parent = _contentTypeService.Get(dt.ParentId); - packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); - if (!File.Exists(packagesFile)) + // could be a container + if (parent != null) { - var xml = new XDocument(new XElement("packages")); - xml.Save(packagesFile); - - return xml; + AddDocumentType(parent, dtl); } + } - var packagesXml = XDocument.Load(packagesFile); - return packagesXml; + if (!dtl.Contains(dt)) + { + dtl.Add(dt); } + } - public void DeleteLocalRepositoryFiles() + private void AddMediaType(IMediaType mediaType, HashSet mediaTypes) + { + if (mediaType.ParentId > 0) { - var packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); - if (File.Exists(packagesFile)) - { - File.Delete(packagesFile); - } + IMediaType? parent = _mediaTypeService.Get(mediaType.ParentId); - var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); - if (Directory.Exists(packagesFolder)) + // could be a container + if (parent != null) { - Directory.Delete(packagesFolder); + 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 de5b8c04ae1c..6ca78967b3b0 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 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 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 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 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 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 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 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 KeyValue = TableNamePrefix + "KeyValue"; - - 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 CreatedPackageSchema = TableNamePrefix + "CreatedPackageSchema"; - } + 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 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 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 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 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 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 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 KeyValue = TableNamePrefix + "KeyValue"; + + 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 CreatedPackageSchema = TableNamePrefix + "CreatedPackageSchema"; } } } diff --git a/src/Umbraco.Core/Persistence/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs index 3c0b2c4d2823..e97f16a66303 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 1a8dbaf97119..e0e507abc1de 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 0f757ae04aa9..6503019988c7 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 b260144de62a..40eb92bef68b 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 f91c4c998b95..2629e14c0445 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 ff766fbe369e..26e1548bc6b7 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 d2a3b0830f64..8803d69fc048 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 3e48a00d0513..fa8e674b971b 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 58daf2e57752..ab6fd4f938e5 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 159267c16e5d..ade100f0d226 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 6d28a86b64db..acceefef5db6 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 e93f5829a1a7..f11ddf10e34a 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. + /// + int CountAll(); + + /// + /// Gets the count of pending cache instructions. + /// + int CountPendingInstructions(int lastId); + + /// + /// 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); + + /// + /// 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); + + /// + /// Deletes cache instructions older than the provided date. /// - public interface ICacheInstructionRepository : IRepository - { - /// - /// Gets the count of pending cache instruction records. - /// - int CountAll(); - - /// - /// Gets the count of pending cache instructions. - /// - int CountPendingInstructions(int lastId); - - /// - /// 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); - - /// - /// 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); - - /// - /// Deletes cache instructions older than the provided date. - /// - void DeleteInstructionsOlderThan(DateTime pruneDate); - } + void DeleteInstructionsOlderThan(DateTime pruneDate); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs index a89ed5628516..7fcdb9d2d9c6 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 b753d355444a..1172512228d8 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 7bdfa294c888..5b122d860d15 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 -{ - // 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 +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 +{ /// - /// Represents the content types common repository, dealing with document, media and member types. + /// Gets and cache all types. /// - public interface IContentTypeCommonRepository - { - /// - /// Gets and cache all types. - /// - IEnumerable? GetAllTypes(); + IEnumerable? GetAllTypes(); - /// - /// Clears the cache. - /// - void ClearCache(); - } + /// + /// Clears the cache. + /// + void ClearCache(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepository.cs index 148132dc2931..77adda58606b 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 2a427da9dd7c..e90c70e89da7 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); - - /// - /// 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(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); - } + TItem? Get(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(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); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDataTypeContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDataTypeContainerRepository.cs index 3e19c08f9993..69caeb8038c7 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 e9063416af22..060d2f2e1d19 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 +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDataTypeRepository : IReadWriteQueryRepository { - public interface IDataTypeRepository : IReadWriteQueryRepository - { - IEnumerable> Move(IDataType toMove, EntityContainer? container); + 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); - } + /// + /// 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 555624b1a0b9..db2347e925d0 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 e5e6e0f41825..12857f05881c 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 e0b7f234ec12..15312ccbf2dd 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); - - /// - /// 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); - - void ClearSchedule(DateTime date, ContentScheduleAction action); - - bool HasContentForExpiration(DateTime date); - bool HasContentForRelease(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); - - /// - /// 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); - - /// - /// 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); - - bool IsPathPublished(IContent? content); - - /// - /// 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); - - /// - /// Assigns a single permission to the current content item for the specified user group ids - /// - /// - /// - /// - void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds); - - /// - /// Gets the explicit list of permissions for the content item - /// - /// - /// - EntityPermissionCollection GetPermissionsForEntity(int entityId); - - /// - /// 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(); - } + /// + /// 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); + + /// + /// 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); + + bool HasContentForExpiration(DateTime date); + + bool HasContentForRelease(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); + + /// + /// 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); + + /// + /// 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); + + bool IsPathPublished(IContent? content); + + /// + /// 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); + + /// + /// Assigns a single permission to the current content item for the specified user group ids + /// + /// + /// + /// + void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds); + + /// + /// Gets the explicit list of permissions for the content item + /// + /// + /// + EntityPermissionCollection GetPermissionsForEntity(int entityId); + + /// + /// 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 53fd62fdbe62..ed604ec16567 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 ee46db369036..7526d83cd0ad 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 a24b76f90a2a..18b2ef1f8e89 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 6b8ece1bfd92..3e2ae8c7b566 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 +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IEntityContainerRepository : IReadRepository, IWriteRepository { - public interface IEntityContainerRepository : IReadRepository, IWriteRepository - { - EntityContainer? Get(Guid id); + EntityContainer? Get(Guid id); - IEnumerable Get(string name, int level); - } + 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 8eeab0b83416..ff7c8f12d972 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); - - IEnumerable GetAll(Guid objectType, params int[] ids); - IEnumerable GetAll(Guid objectType, params Guid[] keys); - - /// - /// Gets entities for a query - /// - /// - /// - IEnumerable GetByQuery(IQuery query); - - /// - /// 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); - } + IEntitySlim? Get(int id); + + IEntitySlim? Get(Guid key); + + IEntitySlim? Get(int id, Guid objectTypeId); + + IEntitySlim? Get(Guid key, Guid objectTypeId); + + IEnumerable GetAll(Guid objectType, params int[] ids); + + IEnumerable GetAll(Guid objectType, params Guid[] keys); + + /// + /// Gets entities for a query + /// + /// + /// + IEnumerable GetByQuery(IQuery query); + + /// + /// 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 7d9594a3c6f4..6d7370768cb7 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 - { +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); - /// - /// 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); - /// - /// 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 0a4b9e76cf05..ec9a79530cdb 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 +{ /// - /// Repository for external logins with Guid as key, so it can be shared for members and users + /// Replaces all external login providers 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 logins); - /// - /// Replaces all external login provider tokens for the providers specified for the user/member key - /// - 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 ce76086ed243..53e1bb40741b 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 77c2f9d40b08..9914e49b26f3 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 +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IFileWithFoldersRepository { - public interface IFileWithFoldersRepository - { - void AddFolder(string folderPath); + void AddFolder(string folderPath); - void DeleteFolder(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 b2c7bc9aa11f..6520644a7f70 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 5dc7ab0555c3..f12bd612fc67 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 c9ee7a9d257d..c9792f009d9c 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 1be32de98972..e7fff03bd7cb 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 8e3d779b9dd5..0d1da11c9d27 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 44ab86b80a0c..9d2fe0ecbfa2 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 -{ - public interface IMacroRepository : IReadWriteQueryRepository, IReadRepository - { - - //IEnumerable GetAll(params string[] aliases); +namespace Umbraco.Cms.Core.Persistence.Repositories; - } +public interface IMacroRepository : IReadWriteQueryRepository, IReadRepository +{ + // IEnumerable GetAll(params string[] aliases); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMacroWithAliasRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMacroWithAliasRepository.cs index 46705d0ded6d..48ead78759ca 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 +namespace Umbraco.Cms.Core.Persistence.Repositories; + +[Obsolete("This interface will be merged with IMacroRepository in Umbraco 11")] +public interface IMacroWithAliasRepository : IMacroRepository { - [Obsolete("This interface will be merged with IMacroRepository in Umbraco 11")] - public interface IMacroWithAliasRepository : IMacroRepository - { - IMacro? GetByAlias(string alias); + IMacro? GetByAlias(string alias); - IEnumerable GetAllByAlias(string[] aliases); - } + IEnumerable GetAllByAlias(string[] aliases); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs index ad268c62924c..d51f031071f1 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 cf2c181d5f5e..fe8c798915f9 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 2a1168ae5776..ac06431ee836 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 a7187ec1ca62..fc12afe1d3ac 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 name - /// - /// - /// - IMemberGroup? GetByName(string? name); - - /// - /// 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(string? username); - - void ReplaceRoles(int[] memberIds, string[] roleNames); - - void AssignRoles(int[] memberIds, string[] roleNames); - - void DissociateRoles(int[] memberIds, string[] roleNames); - - - } + /// + /// Gets a member group by it's uniqueId + /// + /// + /// + IMemberGroup? Get(Guid uniqueId); + + /// + /// 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); + + /// + /// Returns the member groups for a given member + /// + /// + /// + IEnumerable GetMemberGroupsForMember(int memberId); + + /// + /// Returns the member groups for a given member + /// + /// + /// + IEnumerable GetMemberGroupsForMember(string? username); + + void ReplaceRoles(int[] memberIds, string[] roleNames); + + void AssignRoles(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 28a89ff43a5a..58475f802d12 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); - - IMember? GetByUsername(string? username); - - /// - /// 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); - - /// - /// 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); - - /// - /// 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); - } + int[] GetMemberIds(string[] names); + + IMember? GetByUsername(string? username); + + /// + /// 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); + + /// + /// 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); + + /// + /// 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 1ccf3e756c06..255e87220684 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 0b31f0ba46ef..f9cd35534ace 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 4ae191fa7290..5f93a912fcbd 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 be1a00a13077..5a3f63f8cba4 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 c731d39780d5..ba6d24c2d899 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 a8a84079fa53..72b8fa2af05b 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 2190782d3b77..84ef0e92f59e 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 17be5b385681..b6393dfcc030 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 0165b9eb395b..8077a80dc124 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 26dfe4acba73..19929ee83f9c 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 604e1da8d248..f0cfe9490267 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 af3555160efb..5593dec09a5f 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 +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IServerRegistrationRepository : IReadWriteQueryRepository { - public interface IServerRegistrationRepository : IReadWriteQueryRepository - { - void DeactiveStaleServers(TimeSpan staleTimeout); + void DeactiveStaleServers(TimeSpan staleTimeout); - void ClearCache(); - } + void ClearCache(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IStylesheetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IStylesheetRepository.cs index dcdb5debe768..29f132a74a4e 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 e2fa2e4406a6..35c134adb3a8 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 - - /// - /// 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 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); - - #endregion - - #region Queries - - /// - /// Gets a tagged entity. - /// - TaggedEntity? GetTaggedEntityByKey(Guid key); - - /// - /// 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 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 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 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); - - #endregion - } + #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); + + /// + /// 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 property. + /// + /// The identifier of the content item. + /// The identifier of the property type. + void RemoveAll(int contentId, int propertyTypeId); + + #endregion + + #region Queries + + /// + /// Gets a tagged entity. + /// + TaggedEntity? GetTaggedEntityByKey(Guid key); + + /// + /// 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 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 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 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); + + #endregion } diff --git a/src/Umbraco.Core/Persistence/Repositories/ITemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITemplateRepository.cs index 185973623c5c..5c5881ef7a62 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 e6ca8eaa507c..a69722c04a76 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 63622f8e82d2..31a279eb62a6 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 d64f177f1468..7a0d8b6f7460 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 d5cf6fd762b1..0959019af2af 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); - - /// - /// 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); - - /// - /// 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); - - /// - /// 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); - } + /// + /// 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); + + /// + /// 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 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); + + /// + /// 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 8357729f3811..893a3c248e34 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); - - /// - /// 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 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 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); - - /// - /// 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); - - IProfile? GetProfile(string username); - IProfile? GetProfile(int id); - IDictionary GetUserStates(); - - 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); - } + /// + /// 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 + /// + /// + /// + bool ExistsByUserName(string username); + + /// + /// 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 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); + + /// + /// 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); + + IProfile? GetProfile(string username); + + IProfile? GetProfile(int id); + + IDictionary GetUserStates(); + + 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 cd3e31559b58..c30015a7a01f 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 +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public class InstallationRepository : IInstallationRepository { - public class InstallationRepository : IInstallationRepository - { - private readonly IJsonSerializer _jsonSerializer; - private static HttpClient? _httpClient; - private const string RestApiInstallUrl = "https://our.umbraco.com/umbraco/api/Installation/Install"; + 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 InstallationRepository(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; - public async Task SaveInstallLogAsync(InstallLog installLog) + public async Task SaveInstallLogAsync(InstallLog installLog) + { + try { - try + if (_httpClient == null) { - if (_httpClient == null) - _httpClient = new HttpClient(); + _httpClient = new HttpClient(); + } - var content = new StringContent(_jsonSerializer.Serialize(installLog), Encoding.UTF8, "application/json"); + 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) - { } + 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 db0ebd7be5da..a6b6c16aa55a 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 c36156e54b43..4d4e642d9d3c 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 readonly IJsonSerializer _jsonSerializer; - private static HttpClient? _httpClient; - private const string RestApiUpgradeChecklUrl = "https://our.umbraco.com/umbraco/api/UpgradeCheck/CheckUpgrade"; + 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 UpgradeCheckRepository(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; - public async Task CheckUpgradeAsync(SemVersion version) + public async Task CheckUpgradeAsync(SemVersion version) + { + try { - try + if (_httpClient == null) { - if (_httpClient == null) - _httpClient = new HttpClient(); + _httpClient = new HttpClient(); + } - var content = new StringContent(_jsonSerializer.Serialize(new CheckUpgradeDto(version)), Encoding.UTF8, "application/json"); + 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); + _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", "", ""); - } - catch (HttpRequestException) - { - // this occurs if the server for Our is down or cannot be reached - return new UpgradeResult("None", "", ""); - } + return result ?? new UpgradeResult("None", string.Empty, string.Empty); } - private class CheckUpgradeDto + catch (HttpRequestException) { - public CheckUpgradeDto(SemVersion version) - { - VersionMajor = version.Major; - VersionMinor = version.Minor; - VersionPatch = version.Patch; - VersionComment = version.Prerelease; - } + // this occurs if the server for Our is down or cannot be reached + return new UpgradeResult("None", string.Empty, string.Empty); + } + } - public int VersionMajor { get; } - public int VersionMinor { get; } - public int VersionPatch { get; } - public string VersionComment { get; } + 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 8eb27f1a81c5..20db5106d7fe 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); - } - - public static bool SqlIn(this IEnumerable collection, T item) => collection.Contains(item); - - 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); - } + var wildcardmatch = new Regex("^" + Regex.Escape(txt). + + // deal with any wildcard chars % + Replace(@"\%", ".*") + "$"); + + 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 d0f32fb97172..506e516447fc 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 dc0b8d56bd1a..9e3a4dd71b70 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 c799a00df6ef..5e038f0e7660 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 -{ - /// - /// 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!; +namespace Umbraco.Cms.Core.PropertyEditors; - [DataContract] - public class BlockConfiguration - { +/// +/// 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!; - [DataMember(Name ="backgroundColor")] - public string? BackgroundColor { get; set; } + [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] + public NumberRange ValidationLimit { get; set; } = new(); - [DataMember(Name ="iconColor")] - public string? IconColor { 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; } - [DataMember(Name ="thumbnail")] - public string? Thumbnail { get; set; } + [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view.")] + public bool UseInlineEditingAsDefault { get; set; } - [DataMember(Name ="contentElementTypeKey")] - public Guid ContentElementTypeKey { get; set; } + [ConfigurationField("maxPropertyWidth", "Property editor width", "textstring", Description = "optional css overwrite, example: 800px or 100%")] + public string? MaxPropertyWidth { get; set; } - [DataMember(Name ="settingsElementTypeKey")] - public Guid? SettingsElementTypeKey { get; set; } + [DataContract] + public class BlockConfiguration + { + [DataMember(Name = "backgroundColor")] + public string? BackgroundColor { get; set; } - [DataMember(Name ="view")] - public string? View { get; set; } + [DataMember(Name = "iconColor")] + public string? IconColor { get; set; } - [DataMember(Name ="stylesheet")] - public string? Stylesheet { get; set; } + [DataMember(Name = "thumbnail")] + public string? Thumbnail { get; set; } - [DataMember(Name ="label")] - public string? Label { get; set; } + [DataMember(Name = "contentElementTypeKey")] + public Guid ContentElementTypeKey { get; set; } - [DataMember(Name ="editorSize")] - public string? EditorSize { get; set; } + [DataMember(Name = "settingsElementTypeKey")] + public Guid? SettingsElementTypeKey { get; set; } - [DataMember(Name ="forceHideContentEditorInOverlay")] - public bool ForceHideContentEditorInOverlay { get; set; } - } + [DataMember(Name = "view")] + public string? View { get; set; } - [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] - public NumberRange ValidationLimit { get; set; } = new NumberRange(); + [DataMember(Name = "stylesheet")] + public string? Stylesheet { 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 = "editorSize")] + public string? EditorSize { 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; } + [DataMember(Name = "forceHideContentEditorInOverlay")] + public bool ForceHideContentEditorInOverlay { get; set; } + } - [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view.")] - public bool UseInlineEditingAsDefault { get; set; } + [DataContract] + public class NumberRange + { + [DataMember(Name = "min")] + public int? Min { 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 80350bb350d8..02fc30d68b0f 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 89d19c5115bd..25aeb93418e4 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; + Fields = new List(); + _defaultConfiguration = new Dictionary(); + } - /// - /// 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(); + } - /// - /// 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 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}."); - } + /// + [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); + /// + /// 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 + /// + /// Gets the configuration as a typed object. + /// + public static TConfiguration? ConfigurationAs(object? obj) + { + if (obj == null) { - get => _defaultConfiguration; - set => _defaultConfiguration = value; + return default; } - /// - public virtual object? DefaultConfigurationObject => DefaultConfiguration; + if (obj is TConfiguration configuration) + { + return configuration; + } - /// - public virtual bool IsConfiguration(object obj) => obj is IDictionary; + 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 FromDatabase( + string? configurationJson, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + => string.IsNullOrWhiteSpace(configurationJson) + ? new Dictionary() + : configurationEditorJsonSerializer.Deserialize>(configurationJson)!; - /// - public virtual object? FromConfigurationEditor(IDictionary? editorValues, object? configuration) + /// + 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) { - // 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) { - foreach (var key in keys) - { - editorValues?.Remove(key); - } + editorValues?.Remove(key); } + } - return editorValues; + 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(); } - /// - public virtual IDictionary ToConfigurationEditor(object? configuration) + if (!(configuration is IDictionary c)) { - // 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; + throw new ArgumentException( + $"Expecting a {typeof(Dictionary).Name} instance but got {configuration.GetType().Name}.", + nameof(configuration)); } - /// - public virtual IDictionary ToValueEditor(object? configuration) - => ToConfigurationEditor(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 fa2427a048aa..6d64bc2d1911 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.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) - { - var fields = new List(); - var properties = TypeHelper.CachedDiscoverableProperties(typeof(TConfiguration)); + /// + public override IDictionary DefaultConfiguration => + ToConfigurationEditor(DefaultConfigurationObject); + + /// + public override object DefaultConfigurationObject => new TConfiguration(); + + /// + public override bool IsConfiguration(object obj) + => obj is TConfiguration; - foreach (var property in properties) + /// + public override object FromDatabase( + string? configuration, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + { + try + { + if (string.IsNullOrWhiteSpace(configuration)) { - var attribute = property.GetCustomAttribute(false); - if (attribute == null) continue; + return new TConfiguration(); + } - ConfigurationField field; + return configurationEditorJsonSerializer.Deserialize(configuration)!; + } + catch (Exception e) + { + throw new InvalidOperationException( + $"Failed to parse configuration \"{configuration}\" as \"{typeof(TConfiguration).Name}\" (see inner exception).", + e); + } + } - 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); - } + /// + public sealed override object? FromConfigurationEditor( + IDictionary? editorValues, + object? configuration) => FromConfigurationEditor(editorValues, (TConfiguration?)configuration); - // then add it, and overwrite values if they are assigned in the attribute - fields.Add(field); + /// + /// 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); - field.PropertyName = property.Name; - field.PropertyType = property.PropertyType; + /// + 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); - if (!string.IsNullOrWhiteSpace(attribute.Key)) - field.Key = attribute.Key; + /// + /// 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)); - // if the key is still empty then use the property name - if (string.IsNullOrWhiteSpace(field.Key)) - field.Key = property.Name; + foreach (PropertyInfo property in properties) + { + ConfigurationFieldAttribute? attribute = property.GetCustomAttribute(false); + if (attribute == null) + { + continue; + } - if (!string.IsNullOrWhiteSpace(attribute.Name)) - field.Name = attribute.Name; + ConfigurationField field; - if (!string.IsNullOrWhiteSpace(attribute.View)) - field.View = attributeView; + var attributeView = ioHelper.ResolveRelativeOrVirtualUrl(attribute.View); - if (!string.IsNullOrWhiteSpace(attribute.Description)) - field.Description = attribute.Description; + // 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, + }; - if (attribute.HideLabelSettable.HasValue) - field.HideLabel = attribute.HideLabel; + fields.Add(field); + continue; } - return fields; - } + // 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); + } - /// - public override IDictionary DefaultConfiguration => ToConfigurationEditor(DefaultConfigurationObject); + // then add it, and overwrite values if they are assigned in the attribute + fields.Add(field); - /// - public override object DefaultConfigurationObject => new TConfiguration(); + field.PropertyName = property.Name; + field.PropertyType = property.PropertyType; - /// - public override bool IsConfiguration(object obj) - => obj is TConfiguration; + if (!string.IsNullOrWhiteSpace(attribute.Key)) + { + field.Key = attribute.Key; + } - /// - public override object FromDatabase(string? configuration, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - { - try + // if the key is still empty then use the property name + if (string.IsNullOrWhiteSpace(field.Key)) { - if (string.IsNullOrWhiteSpace(configuration)) return new TConfiguration(); - return configurationEditorJsonSerializer.Deserialize(configuration)!; + field.Key = property.Name; } - catch (Exception e) + + if (!string.IsNullOrWhiteSpace(attribute.Name)) { - throw new InvalidOperationException($"Failed to parse configuration \"{configuration}\" as \"{typeof(TConfiguration).Name}\" (see inner exception).", e); + field.Name = attribute.Name; } - } - /// - public sealed override object? FromConfigurationEditor(IDictionary? editorValues, object? configuration) - { - return FromConfigurationEditor(editorValues, (TConfiguration?) configuration); - } + if (!string.IsNullOrWhiteSpace(attribute.View)) + { + field.View = attributeView; + } - /// - /// 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); + if (!string.IsNullOrWhiteSpace(attribute.Description)) + { + field.Description = attribute.Description; + } - /// - public sealed override IDictionary ToConfigurationEditor(object? configuration) - { - return ToConfigurationEditor((TConfiguration?) configuration); + if (attribute.HideLabelSettable.HasValue) + { + field.HideLabel = attribute.HideLabel; + } } - /// - /// 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 0e679f9dc14e..40bd0c0ca9c6 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; + + /// + /// 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()) + { + } + /// - /// Represents a datatype configuration field for editing. + /// Initializes a new instance of the class. /// - [DataContract] - public class ConfigurationField + private ConfigurationField(List validators) { - 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. - /// - private ConfigurationField(List validators) + Validators = validators; + Config = new Dictionary(); + + // 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 79e9655e2510..c504a790be33 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. + /// + /// 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) + { + 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)); + } - /// - /// Initializes a new instance of the class. - /// - public ConfigurationFieldAttribute(Type type) + 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; + } + + /// + /// 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) { - Type = type; - Key = string.Empty; + throw new ArgumentNullException(nameof(name)); } - /// - /// 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(name)) { - 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(name)); } - /// - /// 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 (view == 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(view)); } - /// - /// 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(view)) { - get => HideLabelSettable.ValueOrDefault(false); - set => HideLabelSettable.Set(value); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(view)); } - /// - /// 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 + 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 { - 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 555d6f841881..8cbaecdbdbf5 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 4932030db28a..3bffa4ad6103 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 ContentPickerConfigurationEditor(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(ContentPickerConfiguration.StartNodeId)) + .Config = new Dictionary { { "idType", "udi" } }; + + public override IDictionary ToValueEditor(object? configuration) + { + // get the configuration fields + IDictionary 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 5ca0564e693d..7ef5407c4f28 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.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; + } + + protected override IConfigurationEditor CreateConfigurationEditor() => + new ContentPickerConfigurationEditor(_ioHelper, _editorConfigurationParser); - public ContentPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, + 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); - } - - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); + var asString = value is string str ? str : value?.ToString(); - 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 5619a1bb87ba..115b6a237122 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; - - /// - /// Initializes a new instance of the class. - /// - public DataEditor(IDataValueEditorFactory dataValueEditorFactory, EditorType type = EditorType.PropertyValue) + // 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) { - - // 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) + 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) { - // 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; + return ExplicitValueEditor; } - /// - /// 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 + IDataValueEditor editor = CreateValueEditor(); + if (configuration is not null) { - // 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; + ((DataValueEditor)editor).Configuration = configuration; // TODO: casting is bad } - /// - public virtual IPropertyIndexValueFactory PropertyIndexValueFactory => new DefaultPropertyIndexValueFactory(); + return editor; + } - /// - /// Creates a value editor instance. - /// - /// - protected virtual IDataValueEditor CreateValueEditor() - { - if (Attribute == null) - throw new InvalidOperationException($"The editor is not attributed with {nameof(DataEditorAttribute)}"); + /// + /// + /// + /// 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(); - return DataValueEditorFactory.Create(Attribute); - } + /// + public virtual IPropertyIndexValueFactory PropertyIndexValueFactory => new DefaultPropertyIndexValueFactory(); - /// - /// Creates a configuration editor instance. - /// - protected virtual IConfigurationEditor CreateConfigurationEditor() + /// + /// Creates a value editor instance. + /// + /// + protected virtual IDataValueEditor CreateValueEditor() + { + if (Attribute == null) { - 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; + throw new InvalidOperationException($"The editor is not attributed with {nameof(DataEditorAttribute)}"); } - /// - /// Provides a summary of the PropertyEditor for use with the . - /// - protected virtual string DebuggerDisplay() + 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) { - return $"Name: {Name}, Alias: {Alias}"; + 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 d99acb478169..ce15c66a8043 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; + + /// + /// 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) + { + } /// - /// 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. + /// The view to use to render the editor. + public DataEditorAttribute(string alias, string name, string view) + : this(alias, EditorType.PropertyValue, name, view) { - 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) + } + + /// + /// 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) { - 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; + throw new ArgumentNullException(nameof(alias)); } - /// - /// 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 (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)) { - 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."); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(value)); + } - _valueType = value; + if (!ValueTypes.IsValue(value)) + { + throw new ArgumentOutOfRangeException(nameof(value), value, $"Not a valid {typeof(ValueTypes)} 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; } + _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 0c4ca93fc1d8..40daf7ec7c34 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 4794d37c21c9..36e70f2738f2 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 b8e3e597a4aa..c75844bfa247 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.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; + + /// + /// Initializes a new instance of the class. + /// + public DataValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer? jsonSerializer) // for tests, and manifest + { + _localizedTextService = localizedTextService; + _shortStringHelper = shortStringHelper; + _jsonSerializer = jsonSerializer; + ValueType = ValueTypes.String; + Validators = new List(); + } + /// - /// Represents a value editor. + /// Initializes a new instance of the class. /// - [DataContract] - public class DataValueEditor : IDataValueEditor + public DataValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute) { - private readonly ILocalizedTextService _localizedTextService; - private readonly IShortStringHelper _shortStringHelper; - private readonly IJsonSerializer? _jsonSerializer; - - /// - /// Initializes a new instance of the class. - /// - public DataValueEditor( - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer? jsonSerializer) // for tests, and manifest + 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; + throw new ArgumentException("The attribute does not specify a view.", nameof(attribute)); + } - 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); + } - if (view.StartsWith("~/")) - { - view = ioHelper.ResolveRelativeOrVirtualUrl(view); - } + View = view; + ValueType = attribute.ValueType; + HideLabel = attribute.HideLabel; + } - View = view; - ValueType = attribute.ValueType; - HideLabel = attribute.HideLabel; - } + /// + /// Gets or sets the value editor configuration. + /// + public virtual object? Configuration { get; set; } - /// - /// 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) - { - List? results = null; - var r = Validators.SelectMany(v => v.Validate(value, ValueType, Configuration)).ToList(); - if (r.Any()) { results = r; } + /// + /// Gets the validator used to validate the special property type -level "required". + /// + public virtual IValueRequiredValidator RequiredValidator => new RequiredValidator(_localizedTextService); - // 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. + /// + /// Gets the validator used to validate the special property type -level "format". + /// + public virtual IValueFormatValidator FormatValidator => new RegexValidator(_localizedTextService); - if (required) - { - r = RequiredValidator.ValidateRequired(value, ValueType).ToList(); - if (r.Any()) { if (results == null) results = r; else results.AddRange(r); } - } + /// + /// 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; } - 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); } - } + /// + /// The value type which reflects how it is validated and stored in the database + /// + [DataMember(Name = "valueType")] + public string ValueType { get; set; } - return results ?? Enumerable.Empty(); + /// + /// 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()) + { + results = r; } - /// - /// A collection of validators for the pre value editor - /// - [DataMember(Name = "validation")] - public List Validators { get; private set; } = new List(); - - /// - /// Gets the validator used to validate the special property type -level "required". - /// - public virtual IValueRequiredValidator RequiredValidator => new RequiredValidator(_localizedTextService); - - /// - /// Gets the validator used to validate the special property type -level "format". - /// - public virtual IValueFormatValidator FormatValidator => new RegexValidator(_localizedTextService); - - /// - /// 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; - - /// - /// 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) + // 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) { - // 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)) + r = RequiredValidator.ValidateRequired(value, ValueType).ToList(); + if (r.Any()) { - // 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; + return results ?? Enumerable.Empty(); + } + + /// + /// 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; } - /// - /// 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) + 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) { - var value = property.GetValue(culture, segment); - if (value == null) - { - return string.Empty; - } + 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()) + 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; } + catch + { + // Swallow this exception, we thought it was JSON but it really isn't so continue returning a string + } + } - return stringValue; + 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(); + 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(); - return decimalValue.Success - ? decimalValue.Result.ToString(NumberFormatInfo.InvariantInfo) - : value.ToString(); + return decimalValue.Success + ? decimalValue.Result.ToString(NumberFormatInfo.InvariantInfo) + : value.ToString(); - case ValueStorageType.Date: - var dateValue = value.TryConvertTo(); - if (dateValue.Success == false || dateValue.Result == null) - { - return string.Empty; - } + case ValueStorageType.Date: + Attempt dateValue = value.TryConvertTo(); + if (dateValue.Success == false || dateValue.Result == null) + { + return string.Empty; + } - // Dates will be formatted as yyyy-MM-dd HH:mm:ss - return dateValue.Result.Value.ToIsoString(); + // Dates will be formatted as yyyy-MM-dd HH:mm:ss + return dateValue.Result.Value.ToIsoString(); - default: - throw new ArgumentOutOfRangeException("ValueType was out of range."); - } + 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; - // TODO: the methods below should be replaced by proper property value convert ToXPath usage! + var nodeName = property.PropertyType.Alias.ToSafeAlias(_shortStringHelper); - /// - /// Converts a property to Xml fragments. - /// - public IEnumerable ConvertDbToXml(IProperty property, bool published) + foreach (IPropertyValue pvalue in property.Values) { - published &= property.PropertyType.SupportsPublishing; + var value = published ? pvalue.PublishedValue : pvalue.EditedValue; + if (value == null || (value is string stringValue && string.IsNullOrWhiteSpace(stringValue))) + { + continue; + } - var nodeName = property.PropertyType.Alias.ToSafeAlias(_shortStringHelper); + var xElement = new XElement(nodeName); + if (pvalue.Culture != null) + { + xElement.Add(new XAttribute("lang", pvalue.Culture)); + } - foreach (var pvalue in property.Values) + if (pvalue.Segment != null) { - var value = published ? pvalue.PublishedValue : pvalue.EditedValue; - if (value == null || value is string stringValue && string.IsNullOrWhiteSpace(stringValue)) - continue; + xElement.Add(new XAttribute("segment", pvalue.Segment)); + } - 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); - var xValue = ConvertDbToXml(property.PropertyType, value); - xElement.Add(xValue); + yield return xElement; + } + } - 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)); } - /// - /// 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) + switch (ValueTypes.ToStorageType(ValueType)) { - //check for null or empty value, we don't want to return CDATA if that is the case - if (value == null || value.ToString().IsNullOrWhiteSpace()) - { + 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(); + } + } - 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; } - /// - /// Converts a property value to a string. - /// - public virtual string ConvertDbToString(IPropertyType propertyType, object? value) + switch (ValueTypes.ToStorageType(ValueType)) { - if (value == null) - return string.Empty; + 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; + } - switch (ValueTypes.ToStorageType(ValueType)) + 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) { - 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(); + 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 300bdde67214..86b771bcaad4 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 -{ - public class DataValueEditorFactory : IDataValueEditorFactory - { - private readonly IServiceProvider _serviceProvider; +namespace Umbraco.Cms.Core.PropertyEditors; - public DataValueEditorFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; +public class DataValueEditorFactory : IDataValueEditorFactory +{ + private readonly IServiceProvider _serviceProvider; - public TDataValueEditor Create(params object[] args) - where TDataValueEditor: class, IDataValueEditor - => _serviceProvider.CreateInstance(args); + public DataValueEditorFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; - } + 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 099fa9126f9d..24d6f17eb03b 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 + // 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(); - public IEnumerable GetAllReferences(IPropertyCollection properties, PropertyEditorCollection propertyEditors) + foreach (IProperty p in properties) { - 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 b42ea74e88ea..f2868276537e 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 985d58f06dff..27c144516032 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() => + + // different default values + Format = "YYYY-MM-DD HH:mm:ss"; - public DateTimeConfiguration() - { - // different default values - Format = "YYYY-MM-DD HH:mm:ss"; - } + [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; } - } + [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 36c82175c2fb..d97f7e2c6dbc 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 1e65429b6e53..25cb2c42ed22 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.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 52eefbd40072..1b4a094ca205 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 c940560c90c9..5dc4a3ea5be7 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 5cb11b707105..705ab034fcc2 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 a38ea29e0b1c..b74d9903cf91 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 { - /// - /// Indicates that this is a default property value converter (shipped with Umbraco) - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public class DefaultPropertyValueConverterAttribute : Attribute - { - public DefaultPropertyValueConverterAttribute() - { - DefaultConvertersToShadow = Array.Empty(); - } + public DefaultPropertyValueConverterAttribute() => DefaultConvertersToShadow = Array.Empty(); - public DefaultPropertyValueConverterAttribute(params Type[] convertersToShadow) - { - DefaultConvertersToShadow = convertersToShadow; - } + 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; } - } + /// + /// 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; } } diff --git a/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs index 4d74f4aec2c4..c0132d574dcf 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 93d0b91b1844..15469e1e5126 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 380d54dcadc5..cf3452c1149b 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 e1e528dda20d..2eb507519524 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 +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the email address value editor. +/// +public class EmailAddressConfigurationEditor : ConfigurationEditor { - /// - /// 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()) { - // 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) - { - } + 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 9c2dffb61d2b..e9c8255a193a 100644 --- a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfiguration.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the Eye Dropper picker value editor. +/// +public class EyeDropperColorPickerConfiguration { - /// - /// 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("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; } - } + [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 49611f09b94d..487034a6b19b 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 }, - }; - } + { "showAlpha", configuration?.ShowAlpha ?? false }, { "showPalette", configuration?.ShowPalette ?? false }, + }; - /// - public override EyeDropperColorPickerConfiguration FromConfigurationEditor(IDictionary? editorValues, EyeDropperColorPickerConfiguration? configuration) - { - var showAlpha = true; - var showPalette = true; + /// + 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)) + if (editorValues is not null && editorValues.TryGetValue("showAlpha", out var alpha)) + { + Attempt attempt = alpha.TryConvertTo(); + if (attempt.Success) { - var attempt = alpha.TryConvertTo(); - if (attempt.Success) - showAlpha = attempt.Result; + showAlpha = attempt.Result; } + } - if (editorValues is not null && editorValues.TryGetValue("showPalette", out var palette)) + if (editorValues is not null && editorValues.TryGetValue("showPalette", out var palette)) + { + Attempt attempt = palette.TryConvertTo(); + if (attempt.Success) { - var attempt = palette.TryConvertTo(); - if (attempt.Success) - showPalette = attempt.Result; + showPalette = attempt.Result; } - - return new EyeDropperColorPickerConfiguration - { - ShowAlpha = showAlpha, - ShowPalette = showPalette - }; } + + return new EyeDropperColorPickerConfiguration { ShowAlpha = showAlpha, ShowPalette = showPalette }; } } diff --git a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs index e19a3803343a..076ede0ce5f5 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 -{ - [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 IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; +namespace Umbraco.Cms.Core.PropertyEditors; - // 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) - { - } +[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; - public EyeDropperColorPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser, - EditorType type = EditorType.PropertyValue) - : base(dataValueEditorFactory, type) - { - _ioHelper = ioHelper; - _editorConfigurationParser = 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) + { + } - /// - 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 2b1997459c98..4444466c034f 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 2953e2a1ed2f..289f649b009a 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 e8aa86e5d805..732e2d795a37 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 +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the file upload value editor. +/// +public class FileUploadConfigurationEditor : ConfigurationEditor { - /// - /// 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()) { - // 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) - { - } + 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 0e7b238900d9..d661fa9704f0 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; - } + Config = new Dictionary(); + Alias = string.Empty; + } - [DataMember(Name = "name", IsRequired = true)] - public string? Name { get; set; } + [DataMember(Name = "name", IsRequired = true)] + public string? Name { get; set; } - [DataMember(Name = "nameTemplate")] - public string? NameTemplate { get; set; } + [DataMember(Name = "nameTemplate")] + public string? NameTemplate { get; set; } - [DataMember(Name = "alias", IsRequired = true)] - public string Alias { get; set; } + [DataMember(Name = "alias", IsRequired = true)] + public string Alias { get; set; } - [DataMember(Name = "view", IsRequired = true)] - public string? View{ get; set; } + [DataMember(Name = "view", IsRequired = true)] + public string? View { get; set; } - [DataMember(Name = "render")] - public string? Render { get; set; } + [DataMember(Name = "render")] + public string? Render { get; set; } - [DataMember(Name = "icon", IsRequired = true)] - public string? Icon { get; set; } + [DataMember(Name = "icon", IsRequired = true)] + public string? Icon { get; set; } - [DataMember(Name = "config")] - public IDictionary Config { get; set; } + [DataMember(Name = "config")] + public IDictionary Config { get; set; } - protected bool Equals(GridEditor other) + /// + /// 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 string.Equals(Alias, other.Alias); + return false; } - /// - /// 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(this, obj)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((GridEditor) obj); + return true; } - /// - /// Serves as a hash function for a particular type. - /// - /// - /// A hash code for the current . - /// - public override int GetHashCode() + if (obj.GetType() != GetType()) { - return Alias.GetHashCode(); + 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 b4d41f8e3394..d61dcd0e980d 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 831d5d19fda5..47768838d630 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 dba30aaf6046..6f72f29cf3a6 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 663c7db6d6b0..a2f84cd71ccd 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 d44d732464ae..39d7d7e1309a 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 fd1f2f50d2e9..8c768c295f24 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs @@ -1,18 +1,16 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public interface IDataValueReferenceFactory { - 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); + /// + /// 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(); - } + /// + /// + /// + IDataValueReference GetDataValueReference(); } diff --git a/src/Umbraco.Core/PropertyEditors/IFileExtensionConfig.cs b/src/Umbraco.Core/PropertyEditors/IFileExtensionConfig.cs index 6e9e9221f629..611954395670 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 d32005fb7f9d..fa2e8fa5f63a 100644 --- a/src/Umbraco.Core/PropertyEditors/IFileExtensionConfigItem.cs +++ b/src/Umbraco.Core/PropertyEditors/IFileExtensionConfigItem.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public interface IFileExtensionConfigItem { - public interface IFileExtensionConfigItem - { - int Id { get; set; } + int Id { get; set; } - string? Value { get; set; } - } + string? Value { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs b/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs index d6c20b9cdb82..7e6b0c441040 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 28cf26022f16..31078649a534 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 61f31a85c988..2af36b856f9b 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 a63029fc3dc1..1cff2e7552b1 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 6ac6b46f5040..fd607f405430 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 499a691204c9..37d6b8247573 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 9674eaea9895..60705123296c 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 439bfcdc810c..3bbc34843120 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 b4304fad5933..7d26f8a96cba 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 e7c2114dd2fa..e5d01900c641 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 f243158db31e..be95623b5623 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 28fe05d1515b..f023b86a7880 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 b2a214f729bd..cb5a531f65a3 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 LabelConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { + } - /// - public override LabelConfiguration FromConfigurationEditor(IDictionary? editorValues, LabelConfiguration? configuration) + /// + 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) { - 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 c142b581d00d..d9fd8694e90f 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.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 { + 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()) + { + } + /// - /// Represents a property editor for label properties. + /// Initializes a new instance of the class. /// - [DataEditor( - Cms.Core.Constants.PropertyEditors.Aliases.Label, - "Label", - "readonlyvalue", - Icon = "icon-readonly")] - public class LabelPropertyEditor : DataEditor + public LabelPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(dataValueEditorFactory) { - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; + _ioHelper = ioHelper; + _editorConfigurationParser = 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()) - { - } + /// + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); - /// - /// Initializes a new instance of the class. - /// - public LabelPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser) - : base(dataValueEditorFactory) + /// + 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 055867e80bdd..13f423a328ca 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 + // initialize defaults + PageSize = 10; + OrderBy = "SortOrder"; + OrderDirection = "asc"; - PageSize = 10; - OrderBy = "SortOrder"; - OrderDirection = "asc"; - - BulkActionPermissions = new BulkActionPermissionSettings - { - AllowBulkPublish = true, - AllowBulkUnpublish = true, - AllowBulkCopy = true, - AllowBulkMove = true, - AllowBulkDelete = true - }; - - Layouts = new[] + BulkActionPermissions = new BulkActionPermissionSettings + { + AllowBulkPublish = true, + AllowBulkUnpublish = true, + AllowBulkCopy = true, + AllowBulkMove = true, + AllowBulkDelete = true, + }; + + Layouts = new[] + { + 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" } - }; - - IncludeProperties = new[] + Name = "List", + Icon = "icon-list", + IsSystem = 1, + Selected = true, + Path = "views/propertyeditors/listview/layouts/list/list.html", + }, + new Layout { - 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 } - }; - } + 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 }, + }; + } - [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("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("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(); // TODO: managing defaults? + + [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; } - [ConfigurationField("orderBy", "Order By", "views/propertyeditors/listview/sortby.prevalues.html", - Description = "The default sort order for the list")] - public string OrderBy { get; set; } + [DataMember(Name = "header")] + public string? Header { get; set; } - [ConfigurationField("orderDirection", "Order Direction", "views/propertyeditors/listview/orderDirection.prevalues.html")] - public string OrderDirection { get; set; } + [DataMember(Name = "nameTemplate")] + public string? Template { 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; } + [DataMember(Name = "isSystem")] + public int IsSystem { get; set; } // TODO: bool + } - [ConfigurationField("layouts", "Layouts", "views/propertyeditors/listview/layouts.prevalues.html")] - public Layout[] Layouts { get; set; } + [DataContract] + public class Layout + { + [DataMember(Name = "name")] + public string? Name { 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? + [DataMember(Name = "path")] + public string? Path { get; set; } - [ConfigurationField("icon", "Content app icon", "views/propertyeditors/listview/icon.prevalues.html", Description = "The icon of the listview content app")] + [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; } - - [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; } + [DataMember(Name = "isSystem")] + public int IsSystem { get; set; } // TODO: bool - [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; } - - [DataMember(Name = "isSystem")] - public int IsSystem { get; set; } // TODO: bool - - [DataMember(Name = "selected")] - public bool Selected { get; set; } - } + [DataMember(Name = "selected")] + public bool Selected { get; set; } + } - [DataContract] - public class BulkActionPermissionSettings - { - [DataMember(Name = "allowBulkPublish")] - public bool AllowBulkPublish { get; set; } = true; + [DataContract] + public class BulkActionPermissionSettings + { + [DataMember(Name = "allowBulkPublish")] + public bool AllowBulkPublish { get; set; } = true; - [DataMember(Name = "allowBulkUnpublish")] - public bool AllowBulkUnpublish { get; set; } = true; + [DataMember(Name = "allowBulkUnpublish")] + public bool AllowBulkUnpublish { get; set; } = true; - [DataMember(Name = "allowBulkCopy")] - public bool AllowBulkCopy { get; set; } = true; + [DataMember(Name = "allowBulkCopy")] + public bool AllowBulkCopy { get; set; } = true; - [DataMember(Name = "allowBulkMove")] - public bool AllowBulkMove { 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 d673ce4ee6f0..8ecab6d751fc 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 +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the listview value editor. +/// +public class ListViewConfigurationEditor : ConfigurationEditor { - /// - /// 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()) { - // 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) - { - } + 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 81b1c1fba185..f2a08076b9a4 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) - { - var v = GetByName(name); + 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 - } + // 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) + public IManifestValueValidator GetByName(string name) + { + IManifestValueValidator? v = this.FirstOrDefault(x => x.ValidationName.InvariantEquals(name)); + if (v == null) { - var v = this.FirstOrDefault(x => x.ValidationName.InvariantEquals(name)); - if (v == null) - throw new InvalidOperationException($"Could not find a validator named \"{name}\"."); - return v; + throw new InvalidOperationException($"Could not find a validator named \"{name}\"."); } + + return v; } } diff --git a/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs index 66a967c828ff..044c7f2c0c9a 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 62ddd4c053cb..b11ef08f3041 100644 --- a/src/Umbraco.Core/PropertyEditors/MarkdownConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MarkdownConfiguration.cs @@ -1,18 +1,16 @@ -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// 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; } +namespace Umbraco.Cms.Core.PropertyEditors; - [ConfigurationField("defaultValue", "Default value", "textarea", Description = "If value is blank, the editor will show this")] - public string? DefaultValue { get; set; } +/// +/// 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("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 3f9bc612759c..032bafd12b5c 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 6db2ac552e58..3cabd3a306f2 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 { + 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()) + { + } + /// - /// Represents a markdown editor. + /// Initializes a new instance of the class. /// - [DataEditor( - Constants.PropertyEditors.Aliases.MarkdownEditor, - "Markdown editor", - "markdowneditor", - ValueType = ValueTypes.Text, - Group = Constants.PropertyEditors.Groups.RichContent, - Icon = "icon-code")] - public class MarkdownPropertyEditor : DataEditor + public MarkdownPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(dataValueEditorFactory) { - 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); + _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 8b843fdf8595..11ed4d1afd06 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("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("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 NumberRange(); + [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of medias")] + public NumberRange ValidationLimit { get; set; } = new(); - [DataContract] - public class NumberRange - { - [DataMember(Name = "min")] - public int? Min { get; set; } + [ConfigurationField("startNodeId", "Start node", "mediapicker")] + public Udi? StartNodeId { get; set; } - [DataMember(Name = "max")] - public int? Max { get; set; } - } + [ConfigurationField("enableLocalFocalPoint", "Enable Focal Point", "boolean")] + public bool EnableLocalFocalPoint { get; set; } - [ConfigurationField("startNodeId", "Start node", "mediapicker")] - public Udi? StartNodeId { 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; } + [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; } + [DataContract] + public class NumberRange + { + [DataMember(Name = "min")] + public int? Min { get; set; } - [ConfigurationField("crops", "Image Crops", "views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html", Description = "Local crops, stored on document")] - public CropConfiguration[]? Crops { get; set; } + [DataMember(Name = "max")] + public int? Max { get; set; } + } - [DataContract] - public class CropConfiguration - { - [DataMember(Name = "alias")] - public string? Alias { get; set; } + [DataContract] + public class CropConfiguration + { + [DataMember(Name = "alias")] + public string? Alias { get; set; } - [DataMember(Name = "label")] - public string? Label { get; set; } + [DataMember(Name = "label")] + public string? Label { get; set; } - [DataMember(Name = "width")] - public int Width { 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 c5ab1c403ca0..9ccf64a6f01e 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 { + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MediaPicker3ConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + /// - /// Represents the configuration editor for the media picker value editor. + /// Initializes a new instance of the class. /// - public class MediaPicker3ConfigurationEditor : ConfigurationEditor + public MediaPicker3ConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) { - // 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) - - Field(nameof(MediaPicker3Configuration.StartNodeId)) - .Config = new Dictionary { { "idType", "udi" } }; + // 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.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 d18eeac644d1..055f4fea4d7b 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 a3dbbc04d704..62e9eac43998 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 { + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MediaPickerConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + /// - /// Represents the configuration editor for the media picker value editor. + /// Initializes a new instance of the class. /// - public class MediaPickerConfigurationEditor : ConfigurationEditor + 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" } }; + + public override IDictionary ToValueEditor(object? configuration) { - // 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" } }; - } - - 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["idType"] = "udi"; - - return d; - } + // get the configuration fields + IDictionary d = base.ToValueEditor(configuration); + + // 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 a58203c7b5c5..360ba1b023cb 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 +namespace Umbraco.Cms.Core.PropertyEditors; + +public class MediaUrlGeneratorCollection : BuilderCollectionBase { - public class MediaUrlGeneratorCollection : BuilderCollectionBase + public MediaUrlGeneratorCollection(Func> items) + : base(items) { - public MediaUrlGeneratorCollection(Func> items) - : base(items) - { } + } - public bool TryGetMediaPath(string? propertyEditorAlias, object? value, out string? mediaPath) + 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 57ab93832bcb..0c9bf6070f4c 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 cccf0fe2b7ae..221481328b64 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 6d6fb3a8b7ca..dc0ab648dff6 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 d348d6f22e5b..055bd354fd51 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 ba1d03e7bb4c..c256c7b483f7 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 2825b5b8afa0..c1ca368c4709 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 aa66be9d39ca..a377dae5dba1 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()) + { + } + + 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 + Dictionary output = base.ToConfigurationEditor(configuration); + + output["multiPicker"] = configuration?.MaxNumber > 1; + + return output; + } + + /// + public override IDictionary ToValueEditor(object? configuration) { - // 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 override Dictionary ToConfigurationEditor(MultiNodePickerConfiguration? configuration) - { - // sanitize configuration - var output = base.ToConfigurationEditor(configuration); - - output["multiPicker"] = configuration?.MaxNumber > 1; - - 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; - } + 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 bc48bbdd545b..2dcd0f6e9340 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 caf933e6ad7b..35d51cb94484 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(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("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 f5baa18c0415..f85cafa817bc 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 506b3bebc9c3..6c7f93374de2 100644 --- a/src/Umbraco.Core/PropertyEditors/MultipleTextStringConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MultipleTextStringConfiguration.cs @@ -1,14 +1,12 @@ -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration for a multiple textstring value editor. - /// - public class MultipleTextStringConfiguration - { - // fields are configured in the editor +namespace Umbraco.Cms.Core.PropertyEditors; - public int Minimum { get; set; } +/// +/// Represents the configuration for a multiple textstring value editor. +/// +public class MultipleTextStringConfiguration +{ + // fields are configured in the editor + 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 aed6b5cd00f7..a22eb352c09b 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 -{ - - /// - /// 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; } +namespace Umbraco.Cms.Core.PropertyEditors; - [ConfigurationField("minItems", "Min Items", "number", Description = "Minimum number of items allowed.")] - public int? MinItems { get; set; } +/// +/// 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; } - [ConfigurationField("maxItems", "Max Items", "number", Description = "Maximum number of items allowed.")] - public int? MaxItems { get; set; } + [ConfigurationField("minItems", "Min Items", "number", Description = "Minimum number of items allowed.")] + public int? MinItems { get; set; } - [ConfigurationField("confirmDeletes", "Confirm Deletes", "boolean", Description = "Requires editor confirmation for delete actions.")] - public bool ConfirmDeletes { get; set; } = true; + [ConfigurationField("maxItems", "Max Items", "number", Description = "Maximum number of items allowed.")] + public int? MaxItems { get; set; } - [ConfigurationField("showIcons", "Show Icons", "boolean", Description = "Show the Element Type icons.")] - public bool ShowIcons { get; set; } = true; + [ConfigurationField("confirmDeletes", "Confirm Deletes", "boolean", Description = "Requires editor confirmation for delete actions.")] + public bool ConfirmDeletes { 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; } + [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; } + [DataContract] + public class ContentType + { + [DataMember(Name = "ncAlias")] + public string? Alias { get; set; } - [DataMember(Name = "ncTabAlias")] - public string? TabAlias { 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 bab2038d2d85..5adb06b42f7f 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 +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the nested content value editor. +/// +public class NestedContentConfigurationEditor : ConfigurationEditor { - /// - /// 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()) { - // 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) - { - } + 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 7e91d8e3ee2a..f1d295bc3db6 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 c58c962df4f3..eec435ddf6a5 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 c7d8067fff4d..25bcc38d7ed9 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 65056c75ce5b..2897a8c4ed2c 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); - } + // configure + DefaultConfiguration.Add("multiPicker", "1"); + DefaultConfiguration.Add("minNumber", 0); + DefaultConfiguration.Add("maxNumber", 0); + } - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); - internal class MultipleContentPickerParamateterValueEditor : MultiplePickerParamateterValueEditorBase + 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 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 string UdiEntityType { get; } = Constants.UdiEntityType.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 01bae2ada246..44ff5d94c6e1 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 4a6bab528c07..71f626107b4d 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 5182c1fbd238..8aaea32ab458 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.Services; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; + +internal abstract class MultiplePickerParamateterValueEditorBase : DataValueEditor, IDataValueReference { - 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) { - private readonly IEntityService _entityService; - - public MultiplePickerParamateterValueEditorBase( - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - DataEditorAttribute attribute, - IEntityService entityService) - : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) + 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)) - { - yield return new UmbracoEntityReference(udi); - } + Attempt guidAttempt = _entityService.GetKey(id, UmbracoObjectType); + Guid guid = guidAttempt.Success ? guidAttempt.Result : Guid.Empty; - // 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 (guid != Guid.Empty) { - 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 d39f792971fc..f9485441b934 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 64e310551b1c..913c452fb9ad 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 6441e8cb2463..345a3e49717f 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 9e253d4e41a6..781d072e1042 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 ac275c46e30e..75342371a4dd 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; - - public PropertyCacheCompression( - IPropertyCacheCompressionOptions compressionOptions, - IReadOnlyDictionary contentTypes, - PropertyEditorCollection propertyEditors, - ConcurrentDictionary<(int, string, bool), bool> compressedStoragePropertyEditorCache) - { - _compressionOptions = compressionOptions; - _contentTypes = contentTypes ?? throw new System.ArgumentNullException(nameof(contentTypes)); - _propertyEditors = propertyEditors ?? throw new System.ArgumentNullException(nameof(propertyEditors)); - _isCompressedCache = compressedStoragePropertyEditorCache; - } + _compressionOptions = compressionOptions; + _contentTypes = contentTypes ?? throw new ArgumentNullException(nameof(contentTypes)); + _propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); + _isCompressedCache = compressedStoragePropertyEditorCache; + } - public bool IsCompressed(IReadOnlyContentBase content, string alias, bool published) + public bool IsCompressed(IReadOnlyContentBase content, string alias, bool published) + { + var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias, published), x => { - 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 9c9400861652..c835c0ae958d 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 34f72cf5c075..ff700431d5dd 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 fa57956cdd55..ff92c2012f2b 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 0442ae1b18f8..d73eb5a2eb94 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 IsConverter(IPublishedPropertyType propertyType) + => false; - /// - public virtual bool? IsValue(object? value, PropertyValueLevel level) + /// + 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 9214f1048222..20eb9ae4c40f 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 - { - public PropertyValueConverterCollection(Func> items) : base(items) - { - } + private readonly object _locker = new(); + private Dictionary? _defaultConverters; - private readonly object _locker = new object(); - private Dictionary? _defaultConverters; + public PropertyValueConverterCollection(Func> items) + : base(items) + { + } - private Dictionary DefaultConverters + private Dictionary DefaultConverters + { + get { - get + lock (_locker) { - lock (_locker) + if (_defaultConverters != null) { - if (_defaultConverters != null) - return _defaultConverters; + return _defaultConverters; + } - _defaultConverters = new Dictionary(); + _defaultConverters = new Dictionary(); - foreach (var converter in this) + foreach (IPropertyValueConverter converter in this) + { + DefaultPropertyValueConverterAttribute? attr = converter.GetType().GetCustomAttribute(false); + if (attr != null) { - var attr = converter.GetType().GetCustomAttribute(false); - if (attr != null) - _defaultConverters[converter] = attr.DefaultConvertersToShadow; + _defaultConverters[converter] = attr.DefaultConvertersToShadow; } - - return _defaultConverters; } + + return _defaultConverters; } } + } - internal bool IsDefault(IPropertyValueConverter converter) - => DefaultConverters.ContainsKey(converter); + 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 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 f7bbca2b02d0..6d1e329c7ef9 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 583bf87f3eb9..52389fb92ad4 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 e0bbae88b567..6a80144d0d9a 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 a967ec236775..4e0b5b557d19 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 +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the rich text value editor. +/// +public class RichTextConfigurationEditor : ConfigurationEditor { - /// - /// 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()) { - // 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) - { - } + 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 8d41873a119f..709fb3ce9f86 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 6cd9db839917..586e4cd3afe2 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 +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the slider value editor. +/// +public class SliderConfigurationEditor : ConfigurationEditor { - /// - /// 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()) { - // 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) - { - } + 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 61fa80472d8b..5a9808f22706 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 2f77642e5f2f..f22f9b74c416 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.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) - { - var dictionary = base.ToConfigurationEditor(configuration); + 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)); + } - // 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()!; + public override Dictionary ToConfigurationEditor(TagConfiguration? configuration) + { + Dictionary dictionary = base.ToConfigurationEditor(configuration); - return dictionary; + // 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 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 + dictionary["storageType"] = storageType.ToString()!; - if (editorValues is not null) - { - editorValues["storageType"] = Enum.Parse(typeof(TagsStorageType), (string) editorValues["storageType"]!); - } + return dictionary; + } - return base.FromConfigurationEditor(editorValues, configuration); + 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); } } diff --git a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs index c21ea09ac99d..849d6446a93b 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 86ca35ef6427..8e6355258b36 100644 --- a/src/Umbraco.Core/PropertyEditors/TextAreaConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/TextAreaConfiguration.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the textarea value editor. +/// +public class TextAreaConfiguration { - /// - /// 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("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; } - } + [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 4fa4e7908c71..7ae52825fba7 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 +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the textarea value editor. +/// +public class TextAreaConfigurationEditor : ConfigurationEditor { - /// - /// 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) + // 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) - { - } + 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 cb401cf92a0c..6a0995dccd3a 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 { + public TextOnlyValueEditor( + DataEditorAttribute attribute, + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) + { + } + /// - /// 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 + /// A method used to format the database value to a value that can be used by the editor /// - public class TextOnlyValueEditor : DataValueEditor + /// + /// + /// + /// + /// + /// 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) { - public TextOnlyValueEditor( - DataEditorAttribute attribute, - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper) - : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) - { } + var val = property.GetValue(culture, segment); - /// - /// 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) + 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 60f4169ce60d..74de3fea8e88 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; - } - - private static readonly string[] PropertyTypeAliases = - { - Constants.PropertyEditors.Aliases.TextBox, - Constants.PropertyEditors.Aliases.TextArea - }; - private readonly HtmlLocalLinkParser _linkParser; - private readonly HtmlUrlParser _urlParser; + Constants.PropertyEditors.Aliases.TextBox, Constants.PropertyEditors.Aliases.TextArea, + }; - public override bool IsConverter(IPublishedPropertyType propertyType) - => PropertyTypeAliases.Contains(propertyType.EditorAlias); + private readonly HtmlLocalLinkParser _linkParser; + private readonly HtmlUrlParser _urlParser; - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); + public TextStringValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser) + { + _linkParser = linkParser; + _urlParser = urlParser; + } - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; + public override bool IsConverter(IPublishedPropertyType propertyType) + => PropertyTypeAliases.Contains(propertyType.EditorAlias); - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - if (source == null) return null; - var sourceString = source.ToString(); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); - // ensures string is parsed for {localLink} and URLs are resolved correctly - sourceString = _linkParser.EnsureInternalLinks(sourceString!, preview); - sourceString = _urlParser.EnsureUrls(sourceString); - - return sourceString; - } + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Snapshot; - public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) { - // source should come from ConvertSource and be a string (or null) already - return inter ?? string.Empty; + return null; } - 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; - } + 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 fb56567bc537..26262f35897a 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 81ea1f07b808..69d39a44aba8 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 +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the textbox value editor. +/// +public class TextboxConfigurationEditor : ConfigurationEditor { - /// - /// 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()) { - // 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) - { - } + 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 945e10fd17c1..604f4d3c303a 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 d5210edc872e..72578f7c5ef8 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 +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the boolean value editor. +/// +public class TrueFalseConfigurationEditor : ConfigurationEditor { - /// - /// 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()) { - // 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) - { - } + 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 d8bade11e1c0..4e5fc41d494d 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 3e2a48ffd627..9dce63bf124a 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 17dd8060f5a4..269178c0b03d 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 ac7eb9ff614e..1332b0b03cd0 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 IList ValidationResults { get; } = new List(); + public string ElementTypeAlias { get; } - /// - /// 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 449ef432d8a0..06749c765aaa 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 - { - public ComplexEditorPropertyTypeValidationResult(string propertyTypeAlias) - : base(string.Empty) - { - PropertyTypeAlias = propertyTypeAlias; - } + private readonly List _validationResults = new(); - private readonly List _validationResults = new List(); + public ComplexEditorPropertyTypeValidationResult(string propertyTypeAlias) + : base(string.Empty) => + PropertyTypeAlias = propertyTypeAlias; - 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)}"); + public IReadOnlyList ValidationResults => _validationResults; + + public string PropertyTypeAlias { get; } - _validationResults.Add(validationResult); + 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)}"); } - 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 225963f4612b..6ea03ae60fd5 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 7c15a418d8ac..530935d276c2 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 +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +/// +/// Used to validate if the value is a valid date/time +/// +public class DateTimeValidator : IValueValidator { - /// - /// Used to validate if the value is a valid date/time - /// - public class DateTimeValidator : IValueValidator + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) { - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + // don't validate if empty + if (value == null || value.ToString().IsNullOrWhiteSpace()) { - //don't validate if empty - if (value == null || value.ToString().IsNullOrWhiteSpace()) - { - yield break; - } + yield break; + } - 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" - }); - } + 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 1fb2486e45e3..cc00b4614e90 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 string ValidationName => "Decimal"; - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + /// + 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 8e93e5189e8e..73907a4266f6 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 string ValidationName => "Delimited"; - - /// - /// Gets or sets the configuration, when parsed as . - /// - public DelimitedValueValidatorConfig? Configuration { get; set; } + public DelimitedValueValidatorConfig? Configuration { get; set; } + /// + public string ValidationName => "Delimited"; - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + /// + 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 ?? ","; + 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 delimiter = Configuration?.Delimiter ?? ","; - var regex = (Configuration?.Pattern != null) ? new Regex(Configuration.Pattern) : null; + var s = split[i]; - var stringVal = value.ToString(); - var split = stringVal!.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); - for (var i = 0; i < split.Length; 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 0db537ede582..8b984dc533d7 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 string ValidationName => "Email"; - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) - { - var asString = value == null ? "" : value.ToString(); + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + { + var asString = value == null ? string.Empty : value.ToString(); - var emailVal = new EmailAddressAttribute(); + var emailVal = new EmailAddressAttribute(); - if (asString != string.Empty && emailVal.IsValid(asString) == false) - { - // TODO: localize these! - yield return new ValidationResult("Email is invalid", new[] { "value" }); - } + if (asString != string.Empty && emailVal.IsValid(asString) == false) + { + // 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 351d0de82d54..2123d213f62f 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 +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +/// +/// A validator that validates that the value is a valid integer +/// +public sealed class IntegerValidator : IManifestValueValidator { - /// - /// A validator that validates that the value is a valid integer - /// - public sealed class IntegerValidator : IManifestValueValidator - { - /// - public string ValidationName => "Integer"; + /// + public string ValidationName => "Integer"; - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + /// + 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 ead85c30e48f..5a9032303cf3 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"; + /// + /// Gets or sets the configuration, when parsed as . + /// + public string Configuration + { + get => _regex; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } - /// - /// 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) - { } + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(value)); + } - /// - /// 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; + _regex = value; } + } - /// - /// Gets or sets the configuration, when parsed as . - /// - public string Configuration - { - get => _regex; - 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)); + /// + public string ValidationName => "Regex"; - _regex = value; - } + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + { + if (_regex == null) + { + throw new InvalidOperationException("The validator has not been configured."); } - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + return ValidateFormat(value, valueType, _regex); + } + + /// + public IEnumerable ValidateFormat(object? value, string? valueType, string format) + { + if (format == null) { - if (_regex == null) - { - throw new InvalidOperationException("The validator has not been configured."); - } + throw new ArgumentNullException(nameof(format)); + } - return ValidateFormat(value, valueType, _regex); + if (string.IsNullOrWhiteSpace(format)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(format)); } - /// - public IEnumerable ValidateFormat(object? value, string? valueType, string format) + if (value == null || !new Regex(format).IsMatch(value.ToString()!)) { - 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" }); - } + 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 050ba5a38826..296e8eed361b 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 readonly ILocalizedTextService _textService; - const string ValueCannotBeNull = "Value cannot be null"; - const string ValueCannotBeEmpty = "Value cannot be empty"; - public RequiredValidator(ILocalizedTextService textService) - { - _textService = textService; - } + private const string ValueCannotBeNull = "Value cannot be null"; + private const string ValueCannotBeEmpty = "Value cannot be empty"; + private readonly ILocalizedTextService _textService; - /// - public string ValidationName => "Required"; + public RequiredValidator(ILocalizedTextService textService) => _textService = textService; - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + /// + public string ValidationName => "Required"; + + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) => + ValidateRequired(value, valueType); + + /// + public IEnumerable ValidateRequired(object? value, string? valueType) + { + if (value == null) { - return ValidateRequired(value, valueType); + yield return new ValidationResult( + _textService?.Localize("validation", "invalidNull") ?? ValueCannotBeNull, + new[] { "value" }); + yield break; } - /// - public IEnumerable ValidateRequired(object? value, string? valueType) + if (valueType.InvariantEquals(ValueTypes.Json)) { - if (value == null) + if (value.ToString()?.DetectIsEmptyJson() ?? false) { - yield return new ValidationResult(_textService?.Localize("validation", "invalidNull") ?? ValueCannotBeNull, new[] {"value"}); - yield break; + yield return new ValidationResult( + _textService?.Localize("validation", "invalidEmpty") ?? ValueCannotBeEmpty, new[] { "value" }); } - if (valueType.InvariantEquals(ValueTypes.Json)) - { - if (value.ToString()?.DetectIsEmptyJson() ?? false) - { - - yield return new ValidationResult(_textService?.Localize("validation", "invalidEmpty") ?? ValueCannotBeEmpty, new[] { "value" }); - } - - yield break; - } + yield break; + } - if (value.ToString().IsNullOrWhiteSpace()) - { - yield return new ValidationResult(_textService?.Localize("validation", "invalidEmpty") ?? ValueCannotBeEmpty, new[] { "value" }); - } + if (value.ToString().IsNullOrWhiteSpace()) + { + 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 2aeee98bf4b4..2e5c17fe7ee9 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; + private readonly IJsonSerializer _jsonSerializer; - public CheckboxListValueConverter(IJsonSerializer jsonSerializer) - { - _jsonSerializer = jsonSerializer; - } + public CheckboxListValueConverter(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.CheckBoxList); + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.CheckBoxList); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (IEnumerable); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IEnumerable); - 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) - { - var sourceString = source?.ToString() ?? string.Empty; - - if (string.IsNullOrEmpty(sourceString)) - return Enumerable.Empty(); + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + { + var sourceString = source?.ToString() ?? string.Empty; - return _jsonSerializer.Deserialize(sourceString); + if (string.IsNullOrEmpty(sourceString)) + { + return Enumerable.Empty(); } + + return _jsonSerializer.Deserialize(sourceString); } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs index 126b4516d1ae..eded7b732951 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 - { - Constants.Conventions.Content.InternalRedirectId.ToLower(CultureInfo.InvariantCulture), - Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture) - }; + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - public ContentPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor) => _publishedSnapshotAccessor = publishedSnapshotAccessor; + public ContentPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor) => + _publishedSnapshotAccessor = publishedSnapshotAccessor; - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.ContentPicker); + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.ContentPicker); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof(IPublishedContent); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IPublishedContent); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Elements; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Elements; - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) { - if (source == null) return 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)) + if (source is not string) + { + Attempt attemptConvertInt = source.TryConvertTo(); + if (attemptConvertInt.Success) { - return intValue; + return attemptConvertInt.Result; } + } - var attemptConvertUdi = source.TryConvertTo(); - if (attemptConvertUdi.Success) - return attemptConvertUdi.Result; - return null; + // 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; } - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + 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) { - if (inter == null) - return null; + return null; + } - if ((propertyType.Alias != null && PropertiesToExclude.Contains(propertyType.Alias.ToLower(CultureInfo.InvariantCulture))) == false) + if ((propertyType.Alias != null && + PropertiesToExclude.Contains(propertyType.Alias.ToLower(CultureInfo.InvariantCulture))) == false) + { + IPublishedContent? content; + IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); + if (inter is int id) { - IPublishedContent? content; - var 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; + return content; } - else + } + else + { + if (inter is not GuidUdi udi) { - 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 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 7182719ee11b..79419469640f 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 bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.DateTime); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (DateTime); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(DateTime); - 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) + { + 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 + // 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) + { + Attempt attempt = sourceString.TryConvertTo(); + return attempt.Success == false ? DateTime.MinValue : attempt.Result; + } - public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + // 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) { - // source should come from ConvertSource and be a DateTime already - if (inter is null) - { - return null; - } - return XmlConvert.ToString((DateTime) inter, XmlDateTimeSerializationMode.Unspecified); + 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 06eb23bc70c6..5a7f0a4adc42 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 bool IsConverter(IPublishedPropertyType propertyType) + => Constants.PropertyEditors.Aliases.Decimal.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (decimal); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(decimal); - 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) + { + 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 ea7a8b2301e9..97074b66a338 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 6ea5aae9bbd9..b6bbff3b4196 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 4bffc5a928ef..f0be2754369a 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 9f2c06cdf90e..81f163745a03 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 bool IsConverter(IPublishedPropertyType propertyType) + => Constants.PropertyEditors.Aliases.Label.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + 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 2df96fc3101a..06269ef8e8a0 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"; + // 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; + private readonly IPublishedModelFactory _publishedModelFactory; + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - public MediaPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, - IPublishedModelFactory publishedModelFactory) - { - _publishedSnapshotAccessor = publishedSnapshotAccessor ?? - throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); - _publishedModelFactory = publishedModelFactory; - } + public MediaPickerValueConverter( + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedModelFactory publishedModelFactory) + { + _publishedSnapshotAccessor = publishedSnapshotAccessor ?? + throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); + _publishedModelFactory = publishedModelFactory; + } - public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MediaPicker); + public override bool IsConverter(IPublishedPropertyType propertyType) => + propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MediaPicker); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - { - var isMultiple = IsMultipleDataType(propertyType.DataType); - return isMultiple - ? typeof(IEnumerable) - : typeof(IPublishedContent); - } + 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 PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Snapshot; - private bool IsMultipleDataType(PublishedDataType dataType) + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) { - var config = ConfigurationEditor.ConfigurationAs(dataType.Configuration); - return config?.Multiple ?? false; + return null; } - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, - object? source, bool preview) - { - if (source == null) return null; + Udi[]? nodeIds = source.ToString()? + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) + .Select(UdiParser.Parse) + .ToArray(); + return nodeIds; + } - var nodeIds = source.ToString()? - .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) - .Select(UdiParser.Parse) - .ToArray(); - return nodeIds; - } + 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); + 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(); + var udis = (Udi[]?)source; + var mediaItems = new List(); - if (source == null) return isMultiple ? mediaItems : null; + if (source == null) + { + return isMultiple ? mediaItems : null; + } - if (udis?.Any() ?? false) + 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 2fcaa011fd98..a94da59c3696 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 241b968df92d..8c1226419853 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.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) + { + _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) { - private readonly IMemberService _memberService; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - - public MemberPickerValueConverter( - IMemberService memberService, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IUmbracoContextAccessor umbracoContextAccessor) + if (source == null) { - _memberService = memberService; - _publishedSnapshotAccessor = publishedSnapshotAccessor; - _umbracoContextAccessor = umbracoContextAccessor; + return null; } - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.MemberPicker); + Attempt attemptConvertInt = source.TryConvertTo(); + if (attemptConvertInt.Success) + { + return attemptConvertInt.Result; + } - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; + Attempt attemptConvertUdi = source.TryConvertTo(); + if (attemptConvertUdi.Success) + { + return attemptConvertUdi.Result; + } - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof(IPublishedContent); + return null; + } - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + { + if (source == null) { - 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) + IPublishedContent? member; + IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); + if (source is int id) { - if (source == null) + 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) + { + return member; + } + } + else + { + if (source is not GuidUdi sourceUdi) { - IMember? m = _memberService.GetById(id); - if (m == null) - { - return null; - } - member = publishedSnapshot?.Members?.Get(m); - if (member != null) - { - return member; - } + return null; } - else + + IMember? m = _memberService.GetByKey(sourceUdi.Guid); + if (m == null) { - 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 null; } - return source; + 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 faab6e712ef0..de8965ef3b4b 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.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() + { + Constants.Conventions.Content.InternalRedirectId.ToLower(CultureInfo.InvariantCulture), + Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture), + }; + + private readonly IMemberService _memberService; + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public MultiNodeTreePickerValueConverter( + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IUmbracoContextAccessor umbracoContextAccessor, + IMemberService memberService) { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IMemberService _memberService; + _publishedSnapshotAccessor = publishedSnapshotAccessor ?? + throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); + _umbracoContextAccessor = umbracoContextAccessor; + _memberService = memberService; + } - private static readonly List PropertiesToExclude = new List - { - Constants.Conventions.Content.InternalRedirectId.ToLower(CultureInfo.InvariantCulture), - Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture) - }; - - public MultiNodeTreePickerValueConverter( - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IUmbracoContextAccessor umbracoContextAccessor, - IMemberService 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) { - _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); - _umbracoContextAccessor = umbracoContextAccessor; - _memberService = memberService; + return null; } - public override bool IsConverter(IPublishedPropertyType propertyType) + if (propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker)) { - return propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + Udi[]? nodeIds = source.ToString()? + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) + .Select(UdiParser.Parse) + .ToArray(); + return nodeIds; } - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => IsSingleNodePicker(propertyType) - ? typeof(IPublishedContent) - : typeof(IEnumerable); + return null; + } - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + { + if (source == null) { - 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) + // TODO: Inject an UmbracoHelper and create a GetUmbracoHelper method based on either injected or singleton + if (_umbracoContextAccessor.TryGetUmbracoContext(out _)) { - if (source == null) + if (propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker)) { - return null; - } + var udis = (Udi[])source; + var isSingleNodePicker = IsSingleNodePicker(propertyType); - // 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.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; - } + break; + } - if (multiNodeTreePickerItem != null && multiNodeTreePickerItem.ContentType.ItemType != PublishedItemType.Element) + if (multiNodeTreePickerItem != null && + multiNodeTreePickerItem.ContentType.ItemType != PublishedItemType.Element) + { + multiNodeTreePicker.Add(multiNodeTreePickerItem); + if (isSingleNodePicker) { - multiNodeTreePicker.Add(multiNodeTreePickerItem); - if (isSingleNodePicker) - { - break; - } + break; } } + } - if (isSingleNodePicker) - { - return multiNodeTreePicker.FirstOrDefault(); - } - return multiNodeTreePicker; + if (isSingleNodePicker) + { + return multiNodeTreePicker.FirstOrDefault(); } - // return the first nodeId as this is one of the excluded properties that expects a single id - return udis.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; - } + // 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; + // 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; } - private static bool IsSingleNodePicker(IPublishedPropertyType propertyType) => propertyType.DataType.ConfigurationAs()?.MaxNumber == 1; + return content; } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs index b4ce51c07753..3d631afead2e 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 - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => Constants.PropertyEditors.Aliases.MultipleTextstring.Equals(propertyType.EditorAlias); + private static readonly string[] NewLineDelimiters = { "\r\n", "\r", "\n" }; - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (IEnumerable); + public override bool IsConverter(IPublishedPropertyType propertyType) + => Constants.PropertyEditors.Aliases.MultipleTextstring.Equals(propertyType.EditorAlias); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IEnumerable); - private static readonly string[] NewLineDelimiters = { "\r\n", "\r", "\n" }; + 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) + { + // 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 - // - // + return Enumerable.Empty(); + } - 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); + } - //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(); + } - // 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); - public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + var values = (IEnumerable?)inter; + if (values is not null) { - 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) { - foreach (var value in values) - { - var ee = d.CreateElement("value"); - ee.InnerText = value; - e.AppendChild(ee); - } + XmlElement ee = d.CreateElement("value"); + ee.InnerText = value; + e.AppendChild(ee); } - - return d.CreateNavigator(); } + + return d.CreateNavigator(); } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs index d172e534c4c8..141cfe53ec3d 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 162764fbf5cb..c18363a2db05 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 -{ - [DefaultPropertyValueConverter] - public class RadioButtonListValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.RadioButtonList); +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); +[DefaultPropertyValueConverter] +public class RadioButtonListValueConverter : PropertyValueConverterBase +{ + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.RadioButtonList); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - var attempt = source.TryConvertTo(); + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - if (attempt.Success) - return attempt.Result; + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + Attempt attempt = source.TryConvertTo(); - return null; + if (attempt.Success) + { + return attempt.Result; } + + return null; } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/SimpleTinyMceValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/SimpleTinyMceValueConverter.cs index 1ad867bfd020..7503e6711fd4 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 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; - - 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 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); - } - - 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 bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce; + + 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; + + 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 + source; + + 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 1da3458dabd5..76f5b6226592 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 -{ - [DefaultPropertyValueConverter] - public class SliderValueConverter : PropertyValueConverterBase - { - private readonly IDataTypeService _dataTypeService; - - public SliderValueConverter(IDataTypeService dataTypeService) - { - _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); - } +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Slider); +[DefaultPropertyValueConverter] +public class SliderValueConverter : PropertyValueConverterBase +{ + private static readonly ConcurrentDictionary Storages = new(); + private readonly IDataTypeService _dataTypeService; - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => IsRangeDataType(propertyType.DataType.Id) ? typeof (Range) : typeof (decimal); + public SliderValueConverter(IDataTypeService dataTypeService) => _dataTypeService = + dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public static void ClearCaches() => Storages.Clear(); - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) - { - if (source == null) - return null; + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Slider); - 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 }; - } - } + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => IsRangeDataType(propertyType.DataType.Id) ? typeof(Range) : typeof(decimal); - var valueAttempt = source.ToString().TryConvertTo(); - if (valueAttempt.Success) - return valueAttempt.Result; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - // Something failed in the conversion of the strings to decimals + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + { + if (source == null) + { 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 da5dfd5416a0..3afc5a659670 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; + _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); + _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); + } - public TagsValueConverter(IDataTypeService dataTypeService, IJsonSerializer jsonSerializer) - { - _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 bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Tags); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (IEnumerable); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IEnumerable); - 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) + { + if (source == null) { - 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); + return Array.Empty(); } - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + // if Json storage type deserialize and return as string array + if (JsonStorageType(propertyType.DataType.Id)) { - return (string[]?) source; + var array = source.ToString() is not null + ? _jsonSerializer.Deserialize(source.ToString()!) + : null; + return array ?? Array.Empty(); } - /// - /// 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 + // Otherwise assume CSV storage type and return as string array + return source.ToString()?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + } - return Storages.GetOrAdd(dataTypeId, id => - { - var configuration = _dataTypeService.GetDataType(id)?.ConfigurationAs(); - return configuration?.StorageType == TagsStorageType.Json; - }); - } + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) => (string[]?)source; - private static readonly ConcurrentDictionary Storages = new ConcurrentDictionary(); + /// + /// Discovers if the tags data type is storing its data in a Json format + /// + /// + /// The data type id. + /// + /// + /// The . + /// + private bool JsonStorageType(int dataTypeId) => - public static void ClearCaches() + // 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 => { - Storages.Clear(); - } - } + 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 a554e7d13417..7a9ab907d876 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 6534ce3f14f9..ab7f99e7f8d3 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 bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.Boolean; - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (bool); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(bool); - 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) + { + // 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; - - // this is required for correct true/false handling in nested content elements - if (source is long) - return (long)source == 1; + if (s == "1") + { + return true; + } - if (source is bool) - return (bool)source; + return bool.TryParse(s, out var result) && result; + } - // default value is: false - return false; + if (source is int) + { + return (int)source == 1; } - // default ConvertSourceToObject just returns source ie a boolean value + // this is required for correct true/false handling in nested content elements + if (source is long) + { + return (long)source == 1; + } - public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + if (source is bool) { - // source should come from ConvertSource and be a boolean already - return (bool?)inter ?? false ? "1" : "0"; + 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 61b8a02f0e23..ca727f700816 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 List(); + [ConfigurationField("items", "Configure", "multivalues", Description = "Add, remove or sort values for the list.")] + public List Items { get; set; } = new(); - [DataContract] - public class ValueListItem - { - [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 3a99a70a1418..ac6e6a9bb865 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 Decimal: - return ValueStorageType.Decimal; - - case String: - case Bigint: - return ValueStorageType.Nvarchar; - - case Text: - case Json: - case Xml: - return ValueStorageType.Ntext; - - case DateTime: - case Date: - case Time: - return ValueStorageType.Date; - - default: - throw new ArgumentOutOfRangeException(nameof(valueType), $"Value \"{valueType}\" is not a valid ValueTypes."); - } + case Integer: + return ValueStorageType.Integer; + + case Decimal: + return ValueStorageType.Decimal; + + case String: + case Bigint: + return ValueStorageType.Nvarchar; + + case Text: + case Json: + case Xml: + return ValueStorageType.Ntext; + + case DateTime: + case Date: + case Time: + return ValueStorageType.Date; + + 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 28a0afb6ce63..f272dc49bd7c 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 648041a3a4df..4068bc4477f9 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 58844562a7f7..583daca2f315 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 0555960dfa7e..41443ef1f6f6 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 5a06d88ee59a..0ee2ca38ed4b 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. - /// - 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); - - /// - /// 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 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(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. - /// - /// 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 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. - /// - /// 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. - /// - /// 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. - /// - /// 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. - /// - /// 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 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 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 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 a content type identified by its alias. - /// - /// The content type key. - /// The content type, or null. - IPublishedContentType? GetContentType(Guid key); - } + /// 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); + + /// + /// 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 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(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. + /// + /// 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 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. + /// + /// 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. + /// + /// 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. + /// + /// 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. + /// + /// 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 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 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 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 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 4621adcb821d..7526226302cf 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 1c10776d11c2..b0fd46748ed4 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 1f5344df4c3b..43e629170199 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 . + /// + IPublishedContentCache? Content { get; } + + /// + /// Gets the . + /// + IPublishedMediaCache? Media { get; } + + /// + /// Gets the . + /// + IPublishedMemberCache? Members { 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 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. /// - /// 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; } - - /// - /// Gets the . - /// - IPublishedMediaCache? Media { get; } - - /// - /// Gets the . - /// - IPublishedMemberCache? Members { 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 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); - } + /// 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 3a4b5a24b0a9..0f9cc8fca9ba 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 210739c6a29c..f8d158dce92d 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. - * - */ - - /// - /// 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); + /// 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 5695f0337776..1eb09c8144f4 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 9a59cac9d6a1..2deaf7510843 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 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 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 document tags. - /// - IEnumerable GetAllContentTags(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 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 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 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 tags. + /// + IEnumerable GetAllTags(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 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. + /// + 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 557d5469b69b..0659e835a331 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 - { - 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(); - } + private Dictionary? _cultures; - private Dictionary? _cultures; + 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(); + } - private Dictionary GetCultures() => new Dictionary { { string.Empty, new PublishedCultureInfo(string.Empty, Name, UrlSegment, UpdateDate) } }; + public Guid Version { get; set; } - public int Id { get; set; } + public int ParentId { get; set; } - public Guid Key { get; set; } + public IEnumerable? ChildIds { get; set; } - public int? TemplateId { get; set; } + public int Id { get; set; } - public int SortOrder { get; set; } + public object? this[string alias] + { + get + { + IPublishedProperty? property = GetProperty(alias); + return property == null || property.HasValue() == false ? null : property.GetValue(); + } + } - public string? Name { get; set; } + public Guid Key { get; set; } - public IReadOnlyDictionary Cultures => _cultures ??= GetCultures(); + public int? TemplateId { get; set; } - public string? UrlSegment { get; set; } + public int SortOrder { get; set; } - public int WriterId { get; set; } + public string? Name { get; set; } - public int CreatorId { get; set; } + public IReadOnlyDictionary Cultures => _cultures ??= GetCultures(); - public string Path { get; set; } + public string? UrlSegment { get; set; } - public DateTime CreateDate { get; set; } + public int WriterId { get; set; } - public DateTime UpdateDate { get; set; } + public int CreatorId { get; set; } - public Guid Version { get; set; } + public string Path { get; set; } - public int Level { get; set; } + public DateTime CreateDate { get; set; } - public PublishedItemType ItemType => PublishedItemType.Content; + public DateTime UpdateDate { get; set; } - public bool IsDraft(string? culture = null) => false; + public int Level { get; set; } - public bool IsPublished(string? culture = null) => true; + public PublishedItemType ItemType => PublishedItemType.Content; - public int ParentId { get; set; } + public IPublishedContent? Parent { get; set; } - public IEnumerable? ChildIds { get; set; } + public bool IsDraft(string? culture = null) => false; - public IPublishedContent? Parent { get; set; } + public bool IsPublished(string? culture = null) => true; - public IEnumerable? Children { get; set; } + public IEnumerable? Children { get; set; } - public IEnumerable? ChildrenForAllCultures => Children; + public IEnumerable? ChildrenForAllCultures => Children; - public IPublishedContentType ContentType { get; set; } + public IPublishedContentType ContentType { get; set; } - public IEnumerable Properties { get; set; } + public IEnumerable Properties { get; set; } - public IPublishedProperty? GetProperty(string alias) => Properties?.FirstOrDefault(p => p.Alias.InvariantEquals(alias)); + public IPublishedProperty? GetProperty(string alias) => + Properties?.FirstOrDefault(p => p.Alias.InvariantEquals(alias)); - public IPublishedProperty? GetProperty(string alias, bool recurse) + public IPublishedProperty? GetProperty(string alias, bool recurse) + { + IPublishedProperty? property = GetProperty(alias); + if (recurse == false) { - IPublishedProperty? property = GetProperty(alias); - if (recurse == false) - { - return property; - } - - IPublishedContent? content = this; - while (content != null && (property == null || property.HasValue() == false)) - { - content = content.Parent; - property = content?.GetProperty(alias); - } - 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 abeb19e4ec77..e4e9010f5b94 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 -{ - // 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 Dictionary(); +namespace Umbraco.Cms.Core.PublishedCache.Internal; - public InternalPublishedContentCache() - : base(false) - { - } +// 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 void Add(InternalPublishedContent content) => _content[content.Id] = content.CreateModel(Mock.Of()); + public InternalPublishedContentCache() + : base(false) + { + } - public void Clear() => _content.Clear(); + public IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null) => 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 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(bool preview, int contentId, string? culture = null) => throw new NotImplementedException(); + public string GetRouteById(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, 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, Guid contentId) => throw new NotImplementedException(); + public override IPublishedContent GetById(bool preview, Udi nodeId) => throw new NotSupportedException(); - public override IPublishedContent GetById(bool preview, Udi nodeId) => throw new NotSupportedException(); + public override bool HasById(bool preview, int contentId) => _content.ContainsKey(contentId); - 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 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, string xpath, XPathVariable[] vars) => throw new NotImplementedException(); + public override IPublishedContent GetSingleByXPath(bool preview, XPathExpression 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, string xpath, XPathVariable[] vars) => throw new NotImplementedException(); + public override IEnumerable + GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars) => throw new NotImplementedException(); - public override IEnumerable GetByXPath(bool preview, System.Xml.XPath.XPathExpression xpath, XPathVariable[] vars) => throw new NotImplementedException(); + public override XPathNavigator CreateNavigator(bool preview) => throw new NotImplementedException(); - public override System.Xml.XPath.XPathNavigator CreateNavigator(bool preview) => throw new NotImplementedException(); + public override XPathNavigator CreateNodeNavigator(int id, 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 bool HasContent(bool preview) => _content.Count > 0; + public override IPublishedContentType GetContentType(int id) => throw new NotImplementedException(); - public override IPublishedContentType GetContentType(int id) => throw new NotImplementedException(); + public override IPublishedContentType GetContentType(string alias) => throw new NotImplementedException(); - public override IPublishedContentType GetContentType(string alias) => throw new NotImplementedException(); + public override IPublishedContentType GetContentType(Guid key) => throw new NotImplementedException(); - public override IPublishedContentType GetContentType(Guid key) => throw new NotImplementedException(); + public override IEnumerable GetByContentType(IPublishedContentType contentType) => + 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 0e7280d4434b..d9437e6b8c2b 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 0516edc47b73..015962b5aafd 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 InnerContentCache { get; } = new InternalPublishedContentCache(); - public InternalPublishedContentCache InnerMediaCache { get; } = new InternalPublishedContentCache(); + public InternalPublishedContentCache InnerMediaCache { get; } = new(); - public IPublishedContentCache Content => InnerContentCache; + public IPublishedContentCache Content => InnerContentCache; - public IPublishedMediaCache Media => InnerMediaCache; + public IPublishedMediaCache Media => InnerMediaCache; - public IPublishedMemberCache? Members => null; + public IPublishedMemberCache? Members => null; - public IDomainCache? Domains => null; + public IDomainCache? Domains => null; - public IDisposable ForcedPreview(bool forcedPreview, Action? callback = null) => throw new NotImplementedException(); + public IAppCache? SnapshotCache => null; - public void Resync() - { - } + public IDisposable ForcedPreview(bool forcedPreview, Action? callback = null) => + throw new NotImplementedException(); - public IAppCache? SnapshotCache => null; + public IAppCache? ElementsCache => null; - public IAppCache? ElementsCache => null; + public void Dispose() + { + } - 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 bbf121b45719..09de76ace5e6 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? _snapshot; - private InternalPublishedSnapshot? _previewSnapshot; + private InternalPublishedSnapshot? _previewSnapshot; + private InternalPublishedSnapshot? _snapshot; - public Task CollectAsync() => Task.CompletedTask; + public Task CollectAsync() => Task.CompletedTask; - public IPublishedSnapshot CreatePublishedSnapshot(string? previewToken) + 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 b374424b8b5e..3e961ce434dd 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)); + + protected PublishedCacheBase(bool previewDefault) => PreviewDefault = previewDefault; + + public bool PreviewDefault { get; } - public PublishedCacheBase(IVariationContextAccessor variationContextAccessor) - { - _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); + public abstract IPublishedContent? GetById(bool preview, int contentId); - } - public bool PreviewDefault { get; } + public IPublishedContent? GetById(int contentId) + => GetById(PreviewDefault, contentId); - protected PublishedCacheBase(bool previewDefault) - { - PreviewDefault = previewDefault; - } + public abstract IPublishedContent? GetById(bool preview, Guid contentId); - public abstract IPublishedContent? GetById(bool preview, int contentId); + public IPublishedContent? GetById(Guid contentId) + => GetById(PreviewDefault, contentId); - public IPublishedContent? GetById(int contentId) - => GetById(PreviewDefault, contentId); + public abstract IPublishedContent? GetById(bool preview, Udi contentId); - public abstract IPublishedContent? GetById(bool preview, Guid contentId); + public IPublishedContent? GetById(Udi contentId) + => GetById(PreviewDefault, contentId); - public IPublishedContent? GetById(Guid contentId) - => GetById(PreviewDefault, contentId); + public abstract bool HasById(bool preview, int contentId); - public abstract IPublishedContent? GetById(bool preview, Udi contentId); + public bool HasById(int contentId) + => HasById(PreviewDefault, contentId); - public IPublishedContent? GetById(Udi contentId) - => GetById(PreviewDefault, contentId); + public abstract IEnumerable GetAtRoot(bool preview, string? culture = null); - public abstract bool HasById(bool preview, int contentId); + public IEnumerable GetAtRoot(string? culture = null) => GetAtRoot(PreviewDefault, culture); - public bool HasById(int contentId) - => HasById(PreviewDefault, contentId); + public abstract IPublishedContent? GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars); - public abstract IEnumerable GetAtRoot(bool preview, string? culture = null); + public IPublishedContent? GetSingleByXPath(string xpath, XPathVariable[] vars) => + GetSingleByXPath(PreviewDefault, xpath, vars); - public IEnumerable GetAtRoot(string? culture = null) - { - return GetAtRoot(PreviewDefault, culture); - } + public abstract IPublishedContent? GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars); - public abstract IPublishedContent? GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars); + public IPublishedContent? GetSingleByXPath(XPathExpression xpath, XPathVariable[] vars) => + GetSingleByXPath(PreviewDefault, xpath, vars); - public IPublishedContent? GetSingleByXPath(string xpath, XPathVariable[] vars) - { - return GetSingleByXPath(PreviewDefault, xpath, vars); - } + public abstract IEnumerable GetByXPath(bool preview, string xpath, XPathVariable[] vars); - public abstract IPublishedContent? GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars); + public IEnumerable GetByXPath(string xpath, XPathVariable[] vars) => + GetByXPath(PreviewDefault, xpath, vars); - public IPublishedContent? GetSingleByXPath(XPathExpression xpath, XPathVariable[] vars) - { - return GetSingleByXPath(PreviewDefault, xpath, vars); - } + public abstract IEnumerable + GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars); - public abstract IEnumerable GetByXPath(bool preview, string xpath, XPathVariable[] vars); + public IEnumerable GetByXPath(XPathExpression xpath, XPathVariable[] vars) => + GetByXPath(PreviewDefault, xpath, vars); - public IEnumerable GetByXPath(string xpath, XPathVariable[] vars) - { - return GetByXPath(PreviewDefault, xpath, vars); - } + public abstract XPathNavigator CreateNavigator(bool preview); - public abstract IEnumerable GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars); + public XPathNavigator CreateNavigator() => CreateNavigator(PreviewDefault); - public IEnumerable GetByXPath(XPathExpression xpath, XPathVariable[] vars) - { - return GetByXPath(PreviewDefault, xpath, vars); - } + public abstract XPathNavigator? CreateNodeNavigator(int id, bool preview); - public abstract XPathNavigator CreateNavigator(bool preview); + public abstract bool HasContent(bool preview); - public XPathNavigator CreateNavigator() - { - return CreateNavigator(PreviewDefault); - } + public bool HasContent() => HasContent(PreviewDefault); - public abstract XPathNavigator? CreateNodeNavigator(int id, bool preview); + public abstract IPublishedContentType? GetContentType(int id); - public abstract bool HasContent(bool preview); + public abstract IPublishedContentType? GetContentType(string alias); - public bool HasContent() - { - return HasContent(PreviewDefault); - } + public abstract IPublishedContentType? GetContentType(Guid key); - public abstract IPublishedContentType? GetContentType(int id); - public abstract IPublishedContentType? GetContentType(string alias); - public abstract IPublishedContentType? GetContentType(Guid key); + public virtual IEnumerable GetByContentType(IPublishedContentType contentType) => - 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); - } - } + // 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 c67e3b0e40c1..297a62b589d2 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 - { - // 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) 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); + private readonly IPublishedProperty[] _propertiesArray; - _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 + // 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) + { + 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; } + if (referenceCacheLevel != PropertyCacheLevel.None && publishedSnapshotAccessor == null) + { + throw new ArgumentNullException( + "A published snapshot accessor is required when referenceCacheLevel != None.", + nameof(publishedSnapshotAccessor)); + } - #endregion + ContentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); + Key = key; - #region PublishedElement + values = GetCaseInsensitiveValueDictionary(values); - public Guid Key { get; } + _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]; + } - #endregion + // 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) + { + } - #region Properties + public IPublishedContentType ContentType { get; } - private readonly IPublishedProperty[] _propertiesArray; + public Guid Key { get; } - public IEnumerable Properties => _propertiesArray; + 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 IPublishedProperty? GetProperty(string alias) - { - var index = ContentType.GetPropertyIndex(alias); - var property = index < 0 ? null : _propertiesArray?[index]; - return property; - } + public IEnumerable Properties => _propertiesArray; - #endregion + 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 c6fe365be8c5..6beb094bef65 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; - - protected readonly IPublishedElement Element; - protected readonly bool IsPreviewing; - protected readonly bool IsMember; - - private bool _interInitialized; - private object? _interValue; - private CacheValues? _cacheValues; - private string? _valuesCacheKey; - - // 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) - { - _sourceValue = sourceValue; - _publishedSnapshotAccessor = publishedSnapshotAccessor; - Element = element; - IsPreviewing = previewing; - IsMember = propertyType.ContentType?.ItemType == PublishedItemType.Member; - } + _sourceValue = sourceValue; + _publishedSnapshotAccessor = publishedSnapshotAccessor; + Element = element; + IsPreviewing = previewing; + IsMember = propertyType.ContentType?.ItemType == PublishedItemType.Member; + } - public override bool HasValue(string? culture = null, string? segment = null) - { - var hasValue = PropertyType.IsValue(_sourceValue, PropertyValueLevel.Source); - if (hasValue.HasValue) return hasValue.Value; + // used to cache the CacheValues of this property + // ReSharper disable InconsistentlySynchronizedField + internal string ValuesCacheKey => _valuesCacheKey ??= PropertyCacheValues(Element.Key, Alias, IsPreviewing); - GetCacheLevels(out var cacheLevel, out var referenceCacheLevel); + public static string PropertyCacheValues(Guid contentUid, string typeAlias, bool previewing) => + "PublishedSnapshot.Property.CacheValues[" + (previewing ? "D:" : "P:") + contentUid + ":" + typeAlias + "]"; - lock (_locko) - { - 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; - } - } - - // 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 + // ReSharper restore InconsistentlySynchronizedField + public override bool HasValue(string? culture = null, string? segment = null) + { + var hasValue = PropertyType.IsValue(_sourceValue, PropertyValueLevel.Source); + if (hasValue.HasValue) { - public bool ObjectInitialized; - public object? ObjectValue; - public bool XPathInitialized; - public object? XPathValue; + return hasValue.Value; } - public static string PropertyCacheValues(Guid contentUid, string typeAlias, bool previewing) => "PublishedSnapshot.Property.CacheValues[" + (previewing ? "D:" : "P:") + contentUid + ":" + typeAlias + "]"; + GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel); - private void GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel) + lock (_locko) { - // 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) + var value = GetInterValue(); + hasValue = PropertyType.IsValue(value, PropertyValueLevel.Inter); + if (hasValue.HasValue) { - cacheLevel = PropertyType.CacheLevel; - referenceCacheLevel = cacheLevel; + return hasValue.Value; } - else + + CacheValues cacheValues = GetCacheValues(cacheLevel); + if (!cacheValues.ObjectInitialized) { - cacheLevel = PropertyCacheLevel.Element; - referenceCacheLevel = ReferenceCacheLevel; + cacheValues.ObjectValue = + PropertyType.ConvertInterToObject(Element, referenceCacheLevel, value, IsPreviewing); + cacheValues.ObjectInitialized = true; } + + value = cacheValues.ObjectValue; + return PropertyType.IsValue(value, PropertyValueLevel.Object) ?? false; } + } - 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; - } + public override object? GetSourceValue(string? culture = null, string? segment = null) => _sourceValue; - if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out var publishedSnapshot)) - { - return null; - } + 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; + } + } - return (IsPreviewing == false || FullCacheWhenPreviewing) && IsMember == false - ? publishedSnapshot!.ElementsCache - : publishedSnapshot!.SnapshotCache; + 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; } - private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel) + if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot)) { - 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; + return null; } - private object? GetInterValue() + return (IsPreviewing == false || FullCacheWhenPreviewing) && IsMember == false + ? publishedSnapshot!.ElementsCache + : publishedSnapshot!.SnapshotCache; + } + + private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel) + { + CacheValues cacheValues; + switch (cacheLevel) { - if (_interInitialized) return _interValue; + 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(); + + // 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."); + } + + return cacheValues; + } - _interValue = PropertyType.ConvertSourceToInter(Element, _sourceValue, IsPreviewing); - _interInitialized = true; + private object? GetInterValue() + { + if (_interInitialized) + { 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) + { + GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel); - public override object? GetValue(string? culture = null, string? segment = null) + 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; } + + 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); + public override object? GetXPathValue(string? culture = null, string? segment = null) + { + GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel); - lock (_locko) + lock (_locko) + { + CacheValues cacheValues = GetCacheValues(cacheLevel); + if (cacheValues.XPathInitialized) { - 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.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 7f81d066f2ad..8f3e4fe8271b 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; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; - public UmbracoContextPublishedSnapshotAccessor(IUmbracoContextAccessor umbracoContextAccessor) - { - _umbracoContextAccessor = umbracoContextAccessor; - } + public UmbracoContextPublishedSnapshotAccessor(IUmbracoContextAccessor umbracoContextAccessor) => + _umbracoContextAccessor = umbracoContextAccessor; - public IPublishedSnapshot? PublishedSnapshot + public IPublishedSnapshot? PublishedSnapshot + { + get { - 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) - { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - publishedSnapshot = null; - return false; - } - publishedSnapshot = umbracoContext?.PublishedSnapshot; + set => throw new NotSupportedException(); // not ok to set + } - return publishedSnapshot is not null; + public bool TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) + { + 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 982e0835fba3..a6c58466d27c 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 - { - #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)); + /// 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) + { + FieldInfo field = GetField(fieldName); + return EmitFieldGetter(field); + } - var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - - if (property?.GetMethod != null) - return EmitMethod>(property.GetMethod); + /// + /// 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); + } - if (!mustExist) - return default; + /// + /// 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)); + } - throw new InvalidOperationException($"Could not find getter for {typeof(TDeclaring)}.{propertyName}."); + /// + /// 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)); } - /// - /// 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 (string.IsNullOrWhiteSpace(fieldName)) { - 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)); + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(fieldName)); + } - var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + // 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}."); + } - if (property?.SetMethod != null) - return EmitMethod>(property.SetMethod); + // 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}."); + } - if (!mustExist) - return default; + 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)); + } - throw new InvalidOperationException($"Could not find setter for {typeof(TDeclaring)}.{propertyName}."); + 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)); } - /// - /// 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 (string.IsNullOrWhiteSpace(propertyName)) { - 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)); + 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); + 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 (property?.GetMethod != null) + { + return EmitMethod>(property.GetMethod); + } - if (!mustExist) - return default; + if (!mustExist) + { + return default; + } - throw new InvalidOperationException($"Could not find getter and/or setter for {typeof(TDeclaring)}.{propertyName}."); + 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)); } - /// - /// 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 (string.IsNullOrWhiteSpace(propertyName)) { - if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); + } - if (propertyInfo.GetMethod == null) - throw new ArgumentException("Property has no getter.", nameof(propertyInfo)); + PropertyInfo? property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - return EmitMethod>(propertyInfo.GetMethod); + if (property?.SetMethod != null) + { + return EmitMethod>(property.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 EmitPropertySetter(PropertyInfo propertyInfo) + if (!mustExist) { - if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); + return default; + } - if (propertyInfo.SetMethod == null) - throw new ArgumentException("Property has no setter.", nameof(propertyInfo)); + throw new InvalidOperationException($"Could not find setter for {typeof(TDeclaring)}.{propertyName}."); + } - return EmitMethod>(propertyInfo.SetMethod); + /// + /// 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)); } - /// - /// 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 (string.IsNullOrWhiteSpace(propertyName)) { - if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); + } - if (propertyInfo.GetMethod == null || propertyInfo.SetMethod == null) - throw new ArgumentException("Property has no getter and/or no setter.", nameof(propertyInfo)); + PropertyInfo? property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (property?.GetMethod != null && property.SetMethod != null) + { 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); + EmitMethod>(property.GetMethod), + EmitMethod>(property.SetMethod)); } - /// - /// 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 (!mustExist) { - if (ctor == null) throw new ArgumentNullException(nameof(ctor)); + return default; + } - var (_, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, true); + throw new InvalidOperationException( + $"Could not find getter and/or setter for {typeof(TDeclaring)}.{propertyName}."); + } - return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor); + /// + /// 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)); } - private static TLambda EmitConstructorSafe(Type[] lambdaParameters, Type returned, ConstructorInfo ctor) + if (propertyInfo.GetMethod == null) { - // get type and args - var ctorDeclaring = ctor.DeclaringType; - var ctorParameters = ctor.GetParameters().Select(x => x.ParameterType).ToArray(); + throw new ArgumentException("Property has no getter.", nameof(propertyInfo)); + } - // 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); + return EmitMethod>(propertyInfo.GetMethod); + } - // 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)})."); - } + /// + /// 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)); + } - // emit - return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); + if (propertyInfo.SetMethod == null) + { + throw new ArgumentException("Property has no setter.", nameof(propertyInfo)); } - /// - /// 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) + 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) { - if (method == null) throw new ArgumentNullException(nameof(method)); + throw new ArgumentNullException(nameof(propertyInfo)); + } - // get type and args - var methodDeclaring = method.DeclaringType; - var methodReturned = method.ReturnType; - var methodParameters = method.GetParameters().Select(x => x.ParameterType).ToArray(); + if (propertyInfo.GetMethod == null || propertyInfo.SetMethod == null) + { + throw new ArgumentException("Property has no getter and/or no setter.", nameof(propertyInfo)); + } - var isStatic = method.IsStatic; - var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(isStatic, out var isFunction); + return ( + EmitMethod>(propertyInfo.GetMethod), + EmitMethod>(propertyInfo.SetMethod)); + } - // 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); + /// + /// 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 (methodParameters.Length != lambdaParameters.Length) - ThrowInvalidLambda(method.Name, methodReturned, methodParameters); + if (propertyInfo.SetMethod == null) + { + throw new ArgumentException("Property has no setter.", nameof(propertyInfo)); + } - for (var i = 0; i < methodParameters.Length; i++) - if (!methodParameters[i].IsAssignableFrom(lambdaParameters[i])) - ThrowInvalidLambda(method.Name, methodReturned, methodParameters); + return EmitMethodUnsafe>(propertyInfo.SetMethod); + } - // 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); + #endregion - // 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)})."); - } + #region Constructors - // emit - return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); + /// + /// 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)); } - // 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) + // get the constructor infos + ConstructorInfo? ctor = declaring.GetConstructor( + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null); + if (ctor == null) { - // non-static methods need the declaring type as first arg - var parameters = lambdaParameters; - if (!method.IsStatic) + if (!mustExist) { - parameters = new Type[lambdaParameters.Length + 1]; - parameters[0] = lambdaDeclaring ?? method.DeclaringType!; - Array.Copy(lambdaParameters, 0, parameters, 1, lambdaParameters.Length); + return default; } - // gets the method argument types - var methodArgTypes = GetParameters(method, withDeclaring: !method.IsStatic); + throw new InvalidOperationException( + $"Could not find constructor {declaring}.ctor({string.Join(", ", (IEnumerable)lambdaParameters)})."); + } - // 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(); + // emit + return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor); + } - // create - return (TLambda) (object) dm.CreateDelegate(typeof(TLambda)); + /// + /// 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)); } - #endregion + (_, Type[] lambdaParameters, Type lambdaReturned) = AnalyzeLambda(true, true); - #region Utilities + return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor); + } - // 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); + 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(); - var (declaring, parameters, returned) = AnalyzeLambda(isStatic, out var maybeFunction); + // validate arguments + if (lambdaParameters.Length != ctorParameters.Length) + { + ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); + } - if (isFunction) - { - if (!maybeFunction) - throw new ArgumentException($"Lambda {typeLambda} is an Action, a Func was expected.", nameof(TLambda)); - } - else + for (var i = 0; i < lambdaParameters.Length; i++) + { + // note: relax the constraint with IsAssignableFrom? + if (lambdaParameters[i] != ctorParameters[i]) { - if (maybeFunction) - throw new ArgumentException($"Lambda {typeLambda} is a Func, an Action was expected.", nameof(TLambda)); + ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); } + } - return (declaring, parameters, returned); + if (!returned.IsAssignableFrom(ctorDeclaring)) + { + ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); } - // 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) + // 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) { - isFunction = false; + throw new ArgumentNullException(nameof(ctor)); + } - var typeLambda = typeof(TLambda); + (_, Type[] lambdaParameters, Type lambdaReturned) = AnalyzeLambda(true, true); - 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)); + // emit - unsafe - use lambda's args and assume they are correct + return EmitConstructor(lambdaReturned, lambdaParameters, ctor); + } - return (null, Array.Empty(), typeof(void)); - } + private static TLambda EmitConstructor(Type? declaring, Type[] lambdaParameters, ConstructorInfo ctor) + { + // gets the method argument types + Type[] ctorParameters = GetParameters(ctor); - var genericDefinition = typeLambda.IsGenericType ? typeLambda.GetGenericTypeDefinition() : null; - var name = genericDefinition?.FullName; + // 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(); - if (name == null) - throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda)); + return (TLambda)(object)dm.CreateDelegate(typeof(TLambda)); + } - var isActionOf = name.StartsWith("System.Action`"); - isFunction = name.StartsWith("System.Func`"); + #endregion - if (!isActionOf && !isFunction) - throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda)); + #region Methods - var genericArgs = typeLambda.GetGenericArguments(); - if (genericArgs.Length == 0) - throw new Exception("Panic: Func<> or Action<> has zero generic arguments."); + /// + /// 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); - var i = 0; - var declaring = isStatic ? typeof(void) : genericArgs[i++]; + /// + /// 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)); + } - 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)); + if (string.IsNullOrWhiteSpace(methodName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(methodName)); + } - var parameters = new Type[parameterCount]; - for (var j = 0; j < parameterCount; j++) - parameters[j] = genericArgs[i++]; + (Type? lambdaDeclaring, Type[] lambdaParameters, Type lambdaReturned) = + AnalyzeLambda(true, out var isFunction); - var returned = isFunction ? genericArgs[i] : typeof(void); + // 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; + } - return (declaring, parameters, returned); + throw new InvalidOperationException( + $"Could not find static method {declaring}.{methodName}({string.Join(", ", (IEnumerable)lambdaParameters)})."); } - private static (DynamicMethod, ILGenerator) CreateIlGenerator(Module? module, Type[] arguments, Type? returned) + // 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) { - if (module == null) throw new ArgumentNullException(nameof(module)); - var dm = new DynamicMethod(string.Empty, returned, arguments, module, true); - return (dm, dm.GetILGenerator()); + throw new ArgumentNullException(nameof(method)); } - private static Type[] GetParameters(ConstructorInfo ctor) + // 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))) { - var parameters = ctor.GetParameters(); - var types = new Type[parameters.Length]; - var i = 0; - foreach (var parameter in parameters) - types[i++] = parameter.ParameterType; - return types; + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); } - private static Type[] GetParameters(MethodInfo method, bool withDeclaring) + if (methodParameters.Length != lambdaParameters.Length) { - 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; + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); } - // emits args - private static void EmitLdargs(ILGenerator ilgen, Type[] lambdaArgTypes, Type[] methodArgTypes) + for (var i = 0; i < methodParameters.Length; i++) { - 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 (!methodParameters[i].IsAssignableFrom(lambdaParameters[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*/); + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); } } - // 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 it's a function then the last lambda arg must match the method returned type + if (isFunction && !lambdaReturned.IsAssignableFrom(methodReturned)) { - if (inputType == methodParamType) return; + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); + } - 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."); - } + // emit + return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); + } - // 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 + /// + /// 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 unbox = ilgen.DefineLabel(); + var isStatic = method.IsStatic; + (Type? lambdaDeclaring, Type[] lambdaParameters, Type lambdaReturned) = AnalyzeLambda(isStatic, out _); - //if (!local) - //{ - // ilgen.DeclareLocal(typeof(object)); // declare local var for st/ld loc_0 - // local = true; - //} + // emit - unsafe - use lambda's args and assume they are correct + return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); + } - // stack: value + /// + /// 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)); + } - // 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 + if (string.IsNullOrWhiteSpace(methodName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(methodName)); + } - ilgen.Emit(OpCodes.Dup); // duplicate top of stack + // validate lambda type + (Type? lambdaDeclaring, Type[] lambdaParameters, Type lambdaReturned) = + AnalyzeLambda(false, out var isFunction); - // stack: value ; value + // 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; + } - ilgen.Emit(OpCodes.Isinst, methodParamType); // test, pops value, and pushes either a null ref, or an instance of the type + throw new InvalidOperationException( + $"Could not find method {lambdaDeclaring}.{methodName}({string.Join(", ", (IEnumerable)lambdaParameters)})."); + } - // stack: inst|null ; value + // emit + return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); + } - ilgen.Emit(OpCodes.Ldnull); // push null + // 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); + } - // stack: null ; inst|null ; value + // gets the method argument types + Type[] methodArgTypes = GetParameters(method, !method.IsStatic); - ilgen.Emit(OpCodes.Cgt_Un); // compare what isInst returned to null - pops 2 values, and pushes 1 if greater else 0 + // 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(); - // stack: 0|1 ; value + // create + return (TLambda)(object)dm.CreateDelegate(typeof(TLambda)); + } - ilgen.Emit(OpCodes.Brtrue_S, unbox); // pops value, branches to unbox if true, ie nonzero + #endregion - // stack: value + #region Utilities - ilgen.Convert(methodParamType); // convert + // 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); - // stack: value|converted + (Type? declaring, Type[] parameters, Type returned) = AnalyzeLambda(isStatic, out var maybeFunction); - ilgen.MarkLabel(unbox); - ilgen.Emit(OpCodes.Unbox_Any, methodParamType); + if (isFunction) + { + if (!maybeFunction) + { + throw new ArgumentException($"Lambda {typeLambda} is an Action, a Func was expected.", nameof(TLambda)); } - else + } + 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) { - // 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); + 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."); } - //private static T ToT(object o) - //{ - // return o is T t ? t : (T) System.Convert.ChangeType(o, typeof(T)); - //} + var i = 0; + Type declaring = isStatic ? typeof(void) : genericArgs[i++]; - private static MethodInfo? _convertMethod; - private static MethodInfo? _getTypeFromHandle; + 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)); + } - private static void Convert(this ILGenerator ilgen, Type type) + var parameters = new Type[parameterCount]; + for (var j = 0; j < parameterCount; j++) { + parameters[j] = genericArgs[i++]; + } + + Type returned = isFunction ? genericArgs[i] : typeof(void); - if (_getTypeFromHandle == null) - _getTypeFromHandle = typeof(Type).GetMethod("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(RuntimeTypeHandle) }, null); + return (declaring, parameters, returned); + } + + private static (DynamicMethod, ILGenerator) CreateIlGenerator(Module? module, Type[] arguments, Type? returned) + { + if (module == null) + { + throw new ArgumentNullException(nameof(module)); + } - if (_convertMethod == null) - _convertMethod = typeof(Convert).GetMethod("ChangeType", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(object), typeof(Type) }, null); + var dm = new DynamicMethod(string.Empty, returned, arguments, module, true); + return (dm, dm.GetILGenerator()); + } - ilgen.Emit(OpCodes.Ldtoken, type); - ilgen.CallMethod(_getTypeFromHandle); - ilgen.CallMethod(_convertMethod); + 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; } - // 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) + 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) { - if (outputType == methodReturnedType) return; + types[i++] = method.DeclaringType!; + } - // 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 + foreach (ParameterInfo parameter in parameters) + { + types[i++] = parameter.ParameterType; + } - if (methodReturnedType.IsValueType) + 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) { - 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); + ilgen.Emit(ldargOpCodes[i]); } 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."); + ilgen.Emit(OpCodes.Ldarg, i); } + + // var local = false; + EmitInputAdapter(ilgen, lambdaArgTypes[i], methodArgTypes[i] /*, ref local*/); } + } - private static void ThrowInvalidLambda(string methodName, Type? returned, Type[] args) + // 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) { - throw new ArgumentException($"Lambda {typeof(TLambda)} does not match {methodName}({string.Join(", ", (IEnumerable) args)}):{returned}.", nameof(TLambda)); + return; } - private static void CallMethod(this ILGenerator ilgen, MethodInfo? method) + if (methodParamType.IsValueType) { - if (method is not null) + if (inputType.IsValueType) { - var virt = !method.IsStatic && (method.IsVirtual || !method.IsFinal); - ilgen.Emit(virt ? OpCodes.Callvirt : OpCodes.Call, method); + // 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; } - private static void Return(this ILGenerator ilgen) + // 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) { - ilgen.Emit(OpCodes.Ret); + 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."); + } - #endregion + // 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 21fb3e9832a2..d47680905a2e 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 { + 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) + { + _requestConfig = requestConfig.CurrentValue; + _siteDomainMapper = siteDomainMapper; + _uriUtility = uriUtility; + _publishedValueFallback = publishedValueFallback; + _umbracoContextAccessor = 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 + /// - /// Provides URLs using the umbracoUrlAlias property. + /// Gets the other URLs of a published content. /// - public class AliasUrlProvider : IUrlProvider + /// 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) { - private RequestHandlerSettings _requestConfig; - private readonly ISiteDomainMapper _siteDomainMapper; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly UriUtility _uriUtility; - private readonly IPublishedValueFallback _publishedValueFallback; - - public AliasUrlProvider(IOptionsMonitor requestConfig, ISiteDomainMapper siteDomainMapper, UriUtility uriUtility, IPublishedValueFallback publishedValueFallback, IUmbracoContextAccessor umbracoContextAccessor) + 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 + if (!node.HasProperty(Constants.Conventions.Content.UrlAlias)) + { + yield break; + } - #region GetUrl + // look for domains, walking up the tree + IPublishedContent? n = node; + IEnumerable? domainUris = DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false); - /// - public UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current) + // n is null at root + while (domainUris == null && n != null) { - return null; // we have nothing to say + // move to parent node + n = n.Parent; + domainUris = n == null + ? null + : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false); } - #endregion - - #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) + // determine whether the alias property varies + var varies = node.GetProperty(Constants.Conventions.Content.UrlAlias)!.PropertyType.VariesByCulture(); + + if (domainUris == null) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var node = umbracoContext.Content?.GetById(id); - if (node == 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) + { yield break; + } + + var umbracoUrlName = node.Value(_publishedValueFallback, Constants.Conventions.Content.UrlAlias); + var aliases = umbracoUrlName?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - if (!node.HasProperty(Constants.Conventions.Content.UrlAlias)) + if (aliases == null || aliases.Any() == false) + { yield break; + } - // 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 + foreach (var alias in aliases.Distinct()) { - // move to parent node - n = n.Parent; - domainUris = n == null ? null : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, excludeDefault: false); + 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 "/" - // determine whether the alias property varies - var varies = node.GetProperty(Constants.Conventions.Content.UrlAlias)!.PropertyType.VariesByCulture(); + // 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); - 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) - yield break; - - 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 + #endregion - string CombinePaths(string path1, string path2) - { - string path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2; - return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash); - } + #region Utilities - #endregion + 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 380d7459edfc..c7089f082477 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; - - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByIdPath( - IOptionsMonitor webRoutingSettings, - ILogger logger, - IRequestAccessor requestAccessor, - IUmbracoContextAccessor umbracoContextAccessor) - { - _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 = 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)); + + webRoutingSettings.OnChange(x => _webRoutingSettings = x); + } - 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)) + { + 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 false; - } - if (umbracoContext == null || (umbracoContext != null && umbracoContext.InPreviewMode == false && _webRoutingSettings.DisableFindContentByIdPath)) - { - return false; - } + return Task.FromResult(false); + } - IPublishedContent? node = null; - var path = frequest.AbsolutePathDecoded; + IPublishedContent? node = null; + var path = frequest.AbsolutePathDecoded; - var nodeId = -1; + var nodeId = -1; - // no id if "/" - if (path != "/") + // no id if "/" + if (path != "/") + { + var noSlashPath = path.Substring(1); + + if (int.TryParse(noSlashPath, NumberStyles.Integer, CultureInfo.InvariantCulture, out nodeId) == false) { - var noSlashPath = path.Substring(1); + nodeId = -1; + } - if (int.TryParse(noSlashPath, NumberStyles.Integer, CultureInfo.InvariantCulture, out nodeId) == false) + if (nodeId > 0) + { + if (_logger.IsEnabled(LogLevel.Debug)) { - nodeId = -1; + _logger.LogDebug("Id={NodeId}", nodeId); } - if (nodeId > 0) + node = umbracoContext.Content?.GetById(nodeId); + + if (node != null) { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Id={NodeId}", nodeId); - } - node = umbracoContext?.Content?.GetById(nodeId); + var cultureFromQuerystring = _requestAccessor.GetQueryStringValue("culture"); - if (node != null) + // if we have a node, check if we have a culture in the query string + if (!string.IsNullOrEmpty(cultureFromQuerystring)) { - - 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); - } + // 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); } - else + + frequest.SetPublishedContent(node); + if (_logger.IsEnabled(LogLevel.Debug)) { - nodeId = -1; // trigger message below + _logger.LogDebug("Found node with id={PublishedContentId}", node.Id); } } - } - - if (nodeId == -1) - { - if (_logger.IsEnabled(LogLevel.Debug)) + else { - _logger.LogDebug("Not a node id"); + 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 646d091ebb68..772155177744 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 a200afec6702..99103d8cb6b3 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.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +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 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; + } + /// - /// Provides an implementation of that handles page URL rewrites - /// that are stored when moving, saving, or deleting a node. + /// Tries to find and assign an Umbraco document to a PublishedRequest. /// + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. /// - /// Assigns a permanent redirect notification to the request. + /// Optionally, can also assign the template or anything else on the document request, although that is not + /// required. /// - public class ContentFinderByRedirectUrl : IContentFinder + public Task TryFindContent(IPublishedRequestBuilder frequest) { - private readonly IRedirectUrlService _redirectUrlService; - private readonly ILogger _logger; - private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByRedirectUrl( - IRedirectUrlService redirectUrlService, - ILogger logger, - IPublishedUrlProvider publishedUrlProvider, - IUmbracoContextAccessor umbracoContextAccessor) + 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) - { - 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; + var route = frequest.Domain != null + ? frequest.Domain.ContentId + + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded) + : frequest.AbsolutePathDecoded; - IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(route, frequest.Culture); + IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(route, frequest.Culture); - if (redirectUrl == null) + if (redirectUrl == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("No match for route: {Route}", route); - } - return false; + _logger.LogDebug("No match for route: {Route}", route); } - 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; - } + 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; + 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} with URL '{Url}', redirecting.", route, content?.Id, url); + _logger.LogDebug("Route {Route} matches content {ContentId} which has no URL.", route, redirectUrl.ContentId); } - 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(false); + } - return true; + // 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 e95a0362158d..d2b2a564a70b 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)); + } + + /// + /// Gets the + /// + protected IUmbracoContextAccessor UmbracoContextAccessor { get; } - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByUrl(ILogger logger, IUmbracoContextAccessor 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. + 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; - } + route = frequest.Domain.ContentId + + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded); + } + else + { + route = frequest.AbsolutePathDecoded; + } - 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 Task.FromResult(node != null); + } - IPublishedContent? node = FindContent(frequest, route); - return node != null; + /// + /// 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; } - /// - /// 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 (docreq == null) { - if (!UmbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - return null; - } + throw new ArgumentNullException(nameof(docreq)); + } - if (docreq == null) - { - throw new System.ArgumentNullException(nameof(docreq)); - } - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Test route {Route}", route); - } + 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) + IPublishedContent? node = + umbracoContext.Content?.GetByRoute(umbracoContext.InPreviewMode, route, culture: docreq.Culture); + if (node != null) + { + docreq.SetPublishedContent(node); + if (_logger.IsEnabled(LogLevel.Debug)) { - docreq.SetPublishedContent(node); - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Got content, id={NodeId}", node.Id); - } + _logger.LogDebug("Got content, id={NodeId}", node.Id); } - else + } + else + { + if (_logger.IsEnabled(LogLevel.Debug)) { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("No match."); - } + _logger.LogDebug("No match."); } - - return node; } + + return node; } } diff --git a/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs b/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs index 5a8f6e16fe8a..3a04c2cb5bad 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 +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 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; + } + /// - /// Provides an implementation of that handles page aliases. + /// Tries to find and assign an Umbraco document to a PublishedRequest. /// - /// - /// 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 + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + public Task TryFindContent(IPublishedRequestBuilder frequest) { - private readonly IPublishedValueFallback _publishedValueFallback; - private readonly IVariationContextAccessor _variationContextAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByUrlAlias( - ILogger logger, - IPublishedValueFallback publishedValueFallback, - IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor) + 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)) - { - return false; - } - IPublishedContent? node = null; + node = FindContentByAlias( + umbracoContext.Content, + frequest.Domain != null ? frequest.Domain.ContentId : 0, + frequest.Culture, + frequest.AbsolutePathDecoded); - // no alias if "/" - if (frequest.Uri.AbsolutePath != "/") + if (node != null) { - 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)) { - frequest.SetPublishedContent(node); - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Path '{UriAbsolutePath}' is an alias for id={PublishedContentId}", frequest.Uri.AbsolutePath, node.Id); - } + _logger.LogDebug( + "Path '{UriAbsolutePath}' is an alias for id={PublishedContentId}", frequest.Uri.AbsolutePath, node.Id); } } - - return node != null; } - private IPublishedContent? FindContentByAlias(IPublishedContentCache? cache, int rootNodeId, string? culture, string alias) + return Task.FromResult(node != null); + } + + private IPublishedContent? FindContentByAlias(IPublishedContentCache? cache, int rootNodeId, string? culture, string alias) + { + if (alias == null) { - if (alias == null) - { - throw new ArgumentNullException(nameof(alias)); - } + 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 + // 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; + // 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," + 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) + 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)) { - // 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); - } + return false; + } - if (string.IsNullOrWhiteSpace(v)) + IPublishedProperty? p = c.GetProperty(propertyAlias); + var varies = p?.PropertyType?.VariesByCulture(); + string? v; + if (varies ?? false) + { + if (!c.HasCulture(culture)) { return false; } - v = "," + v.Replace(" ", string.Empty) + ","; - return v.InvariantContains(a1) || v.InvariantContains(a2); + v = c.Value(_publishedValueFallback, propertyAlias, culture); + } + else + { + v = c.Value(_publishedValueFallback, propertyAlias); } - // 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 (string.IsNullOrWhiteSpace(v)) { - IPublishedContent? rootNode = cache?.GetById(rootNodeId); - return rootNode?.Descendants(_variationContextAccessor).FirstOrDefault(x => IsMatch(x, test1, test2)); + return false; } - if (cache is not null) + 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()) { - foreach (IPublishedContent rootContent in cache.GetAtRoot()) + IPublishedContent? c = rootContent.DescendantsOrSelf(_variationContextAccessor) + .FirstOrDefault(x => IsMatch(x, test1, test2)); + if (c != null) { - IPublishedContent? c = rootContent.DescendantsOrSelf(_variationContextAccessor).FirstOrDefault(x => IsMatch(x, test1, test2)); - if (c != null) - { - return c; - } + return c; } } - - return null; } + + return null; } } diff --git a/src/Umbraco.Core/Routing/ContentFinderByUrlAndTemplate.cs b/src/Umbraco.Core/Routing/ContentFinderByUrlAndTemplate.cs index f05985008653..39fc468ceec9 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.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; + + /// + /// Initializes a new instance of the class. + /// + public ContentFinderByUrlAndTemplate( + ILogger logger, + IFileService fileService, + IContentTypeService contentTypeService, + IUmbracoContextAccessor umbracoContextAccessor, + IOptionsMonitor webRoutingSettings) + : base(logger, umbracoContextAccessor) + { + _logger = logger; + _fileService = fileService; + _contentTypeService = contentTypeService; + _webRoutingSettings = webRoutingSettings.CurrentValue; + webRoutingSettings.OnChange(x => _webRoutingSettings = x); + } + /// - /// Provides an implementation of that handles page nice URLs and a template. + /// Tries to find and assign an Umbraco document to a PublishedRequest. /// - /// - /// 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 + /// 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) { - private readonly ILogger _logger; - private readonly IFileService _fileService; - - private readonly IContentTypeService _contentTypeService; - private WebRoutingSettings _webRoutingSettings; - - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByUrlAndTemplate( - ILogger logger, - IFileService fileService, - IContentTypeService contentTypeService, - IUmbracoContextAccessor umbracoContextAccessor, - IOptionsMonitor webRoutingSettings) - : base(logger, umbracoContextAccessor) + var path = frequest.AbsolutePathDecoded; + + 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) + if (_logger.IsEnabled(LogLevel.Debug)) { - path = DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, path); + _logger.LogDebug("No template in path '/'"); } - // no template if "/" - if (path == "/") - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("No template in path '/'"); - } - return false; - } + 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); + // 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); + ITemplate? template = _fileService.GetTemplate(templateAlias); - if (template == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Not a valid template: '{TemplateAlias}'", templateAlias); - } - return false; - } + if (template == null) + { if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("Valid template: '{TemplateAlias}'", templateAlias); + _logger.LogDebug("Not a 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); + return Task.FromResult(false); + } - if (node == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Not a valid route to node: '{Route}'", route); - } - return false; - } + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Valid template: '{TemplateAlias}'", templateAlias); + } - // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings - if (!node.IsAllowedTemplate(_contentTypeService, _webRoutingSettings, template.Id)) + // 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.LogWarning("Alternative template '{TemplateAlias}' is not allowed on node {NodeId}.", template.Alias, node.Id); - frequest.SetPublishedContent(null); // clear - return false; + _logger.LogDebug("Not a valid route to node: '{Route}'", route); } - // got it - frequest.SetTemplate(template); - return true; + 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 8965d9d44764..cc3b711d9882 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 d471acf60c0b..3c8a0e925d22 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 1afda0175c7a..d1c79783f043 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) - { - _mediaPathGenerators = mediaPathGenerators ?? throw new ArgumentNullException(nameof(mediaPathGenerators)); - _uriUtility = uriUtility; - } + /// + public virtual UrlInfo? GetMediaUrl( + IPublishedContent content, + string propertyAlias, + UrlMode mode, + string? culture, + Uri current) + { + IPublishedProperty? prop = content.GetProperty(propertyAlias); - /// - public virtual UrlInfo? GetMediaUrl(IPublishedContent content, - string propertyAlias, UrlMode mode, string? culture, Uri current) + // get the raw source value since this is what is used by IDataEditorWithMediaPath for processing + var value = prop?.GetSourceValue(culture); + if (value == null) { - var prop = content.GetProperty(propertyAlias); + return null; + } - // 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; - } + IPublishedPropertyType? propType = prop?.PropertyType; - var propType = prop?.PropertyType; + if (_mediaPathGenerators.TryGetMediaPath(propType?.EditorAlias, value, out var path)) + { + Uri url = AssembleUrl(path!, current, mode); + return UrlInfo.Url(url.ToString(), culture); + } - if (_mediaPathGenerators.TryGetMediaPath(propType?.EditorAlias, value, out var path)) - { - var url = AssembleUrl(path!, current, mode); - return UrlInfo.Url(url.ToString(), culture); - } + return null; + } - 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)); } - private Uri AssembleUrl(string path, Uri current, UrlMode mode) + // the stored path is absolute so we just return it as is + if (Uri.IsWellFormedUriString(path, UriKind.Absolute)) { - 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; + return new Uri(path); + } - if (current == null) - mode = UrlMode.Relative; // best we can do + Uri uri; - 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)); - } + if (current == null) + { + mode = UrlMode.Relative; // best we can do + } - return _uriUtility.MediaUriFromUmbraco(uri); + 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 25e076434904..d0a238dbb2d4 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.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// 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()) + { + } + + 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 + /// - /// Provides urls. + /// Gets the other URLs of a published content. /// - public class DefaultUrlProvider : IUrlProvider + /// 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) { - 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()) + 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) + // look for domains, walking up the tree + IPublishedContent? n = node; + IEnumerable? domainUris = + DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false); + + // n is null at root + while (domainUris == null && n != null) { - _requestSettings = requestSettings.CurrentValue; - _logger = logger; - _siteDomainMapper = siteDomainMapper; - _umbracoContextAccessor = umbracoContextAccessor; - _uriUtility = uriUtility; - _localizationService = localizationService; - - requestSettings.OnChange(x => _requestSettings = x); + 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; - } + 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); - } + foreach (DomainAndUri d in domainUris) + { + var culture = d.Culture; - // no domains = exit - if (domainUris == null) + // 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) { - yield break; + continue; } - foreach (DomainAndUri d in domainUris) - { - var culture = d.Culture; + // 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); - // 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; - } + var uri = new Uri(CombinePaths(d.Uri.GetLeftPart(UriPartial.Path), path)); + uri = _uriUtility.UriFromUmbraco(uri, _requestSettings); + yield return UrlInfo.Url(uri.ToString(), culture); + } + } - // 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); + #endregion - var uri = new Uri(CombinePaths(d.Uri.GetLeftPart(UriPartial.Path), path)); - uri = _uriUtility.UriFromUmbraco(uri, _requestSettings); - yield return UrlInfo.Url(uri.ToString(), culture); - } + #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)); } - #endregion + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - #region GetUrl + // will not use cache if previewing + var route = umbracoContext.Content?.GetRouteById(content.Id, culture); - /// - public virtual UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current) + 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)) { - if (!current.IsAbsoluteUri) - { - throw new ArgumentException("Current URL must be absolute.", nameof(current)); - } + _logger.LogDebug( + "Couldn't find any page with nodeId={NodeId}. This is most likely caused by the page not being published.", + id); + return null; + } - IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - // will not use cache if previewing - var route = umbracoContext.Content?.GetRouteById(content.Id, culture); + // 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); - return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, 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); } - internal UrlInfo? GetUrlFromRoute( - string? route, - IUmbracoContext umbracoContext, - int id, - Uri current, - UrlMode mode, - string? culture) + 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 (string.IsNullOrWhiteSpace(route)) + if (current == null) { - _logger.LogDebug( - "Couldn't find any page with nodeId={NodeId}. This is most likely caused by the page not being published.", - id); - return null; + mode = UrlMode.Relative; // best we can do } - // extract domainUri and path - // route is / or / - 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)) + switch (mode) { - var url = AssembleUrl(domainUri, path, current, mode).ToString(); - return UrlInfo.Url(url, culture); + 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 null; } - #endregion - - #region Utilities - - private Uri AssembleUrl(DomainAndUri? domainUri, string path, Uri current, UrlMode mode) + // a domain was found + else { - Uri uri; - - // ignore vdir at that point, UriFromUmbraco will do it - - if (domainUri == null) // no domain was found + if (mode == UrlMode.Auto) { - if (current == null) + // 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; // best we can do + mode = UrlMode.Relative; } - - switch (mode) + else { - 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)); + mode = UrlMode.Absolute; } } - 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)); - } + 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); - } + // UriFromUmbraco will handle vdir + // meaning it will add vdir into domain URLs too! + return _uriUtility.UriFromUmbraco(uri, _requestSettings); + } - #endregion + 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 ecefb07e8b40..291d7beed920 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; - } + 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; - } + /// + /// 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 unique identifier of the domain. + /// + public int Id { get; } - /// - /// Gets the name of the domain. - /// - public string Name { 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 identifier of the content which supports the domain. + /// + public int ContentId { get; } - /// - /// Gets the culture of the domain. - /// - public string? Culture { 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; } - } + /// + /// 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 751c4ead5891..c5f9497d77b2 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 9e762a600e52..f31244d2acf1 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 @@ public static class DomainUtilities 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 @@ public static class DomainUtilities 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 @@ public static class DomainUtilities { // 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 @@ public static class DomainUtilities { // 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 @@ public static class DomainUtilities // nothing = no magic, return null if (domainsAndUris is null || domainsAndUris.Count == 0) + { return null; + } // sanitize cultures culture = culture?.NullOrWhiteSpaceAsNull(); @@ -179,27 +200,31 @@ public static class DomainUtilities // 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 @@ private static IReadOnlyCollection SelectByBase(IReadOnlyCollectio { // 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 @@ private static IReadOnlyCollection SelectByBase(IReadOnlyCollectio 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 @@ private static DomainAndUri GetByCulture(IReadOnlyCollection domai 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 ab160715bbff..3e4304fe709e 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 19e5f8024636..ad3959ae42d1 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 4478f60334c3..9d944efff77e 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 9f68c618d23a..645de414d702 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 e5a915d682c5..f6cdafee78d1 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 - /// - 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; } - - /// - /// Gets the URI decoded absolute path of the - /// - string AbsolutePathDecoded { get; } - - /// - /// Gets the assigned (if any) - /// - DomainAndUri? Domain { 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 the content request http response status code. - /// - int? ResponseStatusCode { get; } - - /// - /// Gets the current assigned (if any) - /// - IPublishedContent? PublishedContent { get; } - - /// - /// Gets the template assigned to the request (if any) - /// - ITemplate? Template { get; } - - /// - /// Builds the - /// - IPublishedRequest Build(); - - /// - /// 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 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); - - /// - /// 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); - - /// - /// 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); - - /// - /// 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 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); - - /// - /// 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(); - } + /// 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; } + + /// + /// Gets the URI decoded absolute path of the + /// + string AbsolutePathDecoded { get; } + + /// + /// Gets the assigned (if any) + /// + DomainAndUri? Domain { 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 the content request http response status code. + /// + int? ResponseStatusCode { get; } + + /// + /// Gets the current assigned (if any) + /// + IPublishedContent? PublishedContent { get; } + + /// + /// Gets the template assigned to the request (if any) + /// + ITemplate? Template { get; } + + /// + /// Builds the + /// + IPublishedRequest Build(); + + /// + /// 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 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); + + /// + /// 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); + + /// + /// 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); + + /// + /// 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 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); + + /// + /// 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 a3c041768f40..5434c464474d 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 fd52bc78057f..598ad1b53591 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 e9ca34477cde..93afe32d9359 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 0223b39c1d6d..38f28b37644a 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 264be41d6076..85b864d717be 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 d778540e3110..ba0a9b9fc238 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 50328cbfddc2..e3fc3818ef9a 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; - } + 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 Uri Uri { get; } - /// - public string AbsolutePathDecoded { get; } + /// + public string AbsolutePathDecoded { get; } - /// - public bool IgnorePublishedContentCollisions { get; } + /// + public bool IgnorePublishedContentCollisions { get; } - /// - public IPublishedContent? PublishedContent { get; } + /// + public IPublishedContent? PublishedContent { get; } - /// - public bool IsInternalRedirect { get; } + /// + public bool IsInternalRedirect { get; } - /// - public ITemplate? Template { get; } + /// + public ITemplate? Template { get; } - /// - public DomainAndUri? Domain { get; } + /// + public DomainAndUri? Domain { get; } - /// - public string? Culture { get; } + /// + public string? Culture { get; } - /// - public string? RedirectUrl { get; } + /// + public string? RedirectUrl { get; } - /// - public int? ResponseStatusCode { get; } + /// + public int? ResponseStatusCode { get; } - /// - public IReadOnlyList? CacheExtensions { get; } + /// + public IReadOnlyList? CacheExtensions { get; } - /// - public IReadOnlyDictionary? Headers { get; } + /// + public IReadOnlyDictionary? Headers { get; } - /// - public bool SetNoCacheHeader { get; } - } + /// + public bool SetNoCacheHeader { get; } } diff --git a/src/Umbraco.Core/Routing/PublishedRequestBuilder.cs b/src/Umbraco.Core/Routing/PublishedRequestBuilder.cs index 128c81f605f7..180033dd33a8 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; - - /// - /// Initializes a new instance of the class. - /// - public PublishedRequestBuilder(Uri uri, IFileService fileService) - { - Uri = uri; - AbsolutePathDecoded = uri.GetAbsolutePathDecoded(); - _fileService = fileService; - } + Uri = uri; + AbsolutePathDecoded = uri.GetAbsolutePathDecoded(); + _fileService = fileService; + } - /// - public Uri Uri { get; } + /// + public Uri Uri { get; } - /// - public string AbsolutePathDecoded { get; } + /// + public string AbsolutePathDecoded { get; } - /// - public DomainAndUri? Domain { get; private set; } + /// + public DomainAndUri? Domain { get; private set; } - /// - public string? Culture { get; private set; } + /// + public string? Culture { get; private set; } - /// - public ITemplate? Template { get; private set; } + /// + public ITemplate? Template { get; private set; } - /// - public bool IsInternalRedirect { get; private set; } + /// + public bool IsInternalRedirect { get; private set; } - /// - public int? ResponseStatusCode => _responseStatus.HasValue ? (int?)_responseStatus : null; + /// + public int? ResponseStatusCode => _responseStatus.HasValue ? (int?)_responseStatus : null; - /// - public IPublishedContent? PublishedContent + /// + public IPublishedContent? PublishedContent + { + get => _publishedContent; + private set { - get => _publishedContent; - private set - { - _publishedContent = value; - IsInternalRedirect = false; - Template = null; - } + _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 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 SetCacheExtensions(IEnumerable cacheExtensions) + { + _cacheExtensions = cacheExtensions.ToList(); + return this; + } - /// - public IPublishedRequestBuilder SetCulture(string? culture) - { - Culture = culture; - 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 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. - /// - public IPublishedRequestBuilder SetHeaders(IReadOnlyDictionary headers) + // redirecting to self + if (PublishedContent != null && content.Id == PublishedContent.Id) { - _headers = headers; + // no need to set PublishedContent, we're done + IsInternalRedirect = true; return this; } - /// - public IPublishedRequestBuilder SetInternalRedirect(IPublishedContent content) - { - // unless a template has been set already by the finder, - // template should be null at that point. + // else - // redirecting to self - if (PublishedContent != null && content.Id == PublishedContent.Id) - { - // no need to set PublishedContent, we're done - IsInternalRedirect = true; - return this; - } + // set published content - this resets the template, and sets IsInternalRedirect to false + PublishedContent = content; + IsInternalRedirect = true; - // else + return this; + } - // set published content - this resets the template, and sets IsInternalRedirect to false - PublishedContent = content; - IsInternalRedirect = true; + /// + public IPublishedRequestBuilder SetPublishedContent(IPublishedContent? content) + { + PublishedContent = content; + IsInternalRedirect = false; + return this; + } - return this; - } + /// + public IPublishedRequestBuilder SetRedirect(string url, int status = (int)HttpStatusCode.Redirect) + { + _redirectUrl = url; + _responseStatus = (HttpStatusCode)status; + return this; + } - /// - public IPublishedRequestBuilder SetPublishedContent(IPublishedContent? content) - { - PublishedContent = content; - IsInternalRedirect = false; - return this; - } + /// + public IPublishedRequestBuilder SetRedirectPermanent(string url) + { + _redirectUrl = url; + _responseStatus = HttpStatusCode.Moved; + return this; + } - /// - public IPublishedRequestBuilder SetRedirect(string url, int status = (int)HttpStatusCode.Redirect) - { - _redirectUrl = url; - _responseStatus = (HttpStatusCode)status; - return this; - } + /// + public IPublishedRequestBuilder SetResponseStatus(int code) + { + _responseStatus = (HttpStatusCode)code; + return this; + } - /// - public IPublishedRequestBuilder SetRedirectPermanent(string url) - { - _redirectUrl = url; - _responseStatus = HttpStatusCode.Moved; - return this; - } + /// + public IPublishedRequestBuilder SetTemplate(ITemplate? template) + { + Template = template; + return this; + } - /// - public IPublishedRequestBuilder SetResponseStatus(int code) + /// + public bool TrySetTemplate(string alias) + { + if (string.IsNullOrWhiteSpace(alias)) { - _responseStatus = (HttpStatusCode)code; - return this; + Template = null; + return true; } - /// - public IPublishedRequestBuilder SetTemplate(ITemplate? template) - { - Template = template; - return this; - } + // NOTE - can we still get it with whitespaces in it due to old legacy bugs? + alias = alias.Replace(" ", string.Empty); - /// - public bool TrySetTemplate(string alias) + ITemplate? model = _fileService.GetTemplate(alias); + if (model == null) { - 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; - return true; + return false; } - /// - public void IgnorePublishedContentCollisions() => _ignorePublishedContentCollisions = true; + 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 855bd53bded6..6b9720e4ace2 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 44a75aaccd9d..c7167971dfb3 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; } + UmbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); + _publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter)); + _webRoutingSettings = webRoutingSettings.Value; + Uri = uri ?? umbracoContext.CleanedUmbracoUrl; + } - // utility for ensuring it is ok to set some properties - public void EnsureWriteable() + /// + /// 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 { - if (_readonly) - { - throw new InvalidOperationException("Cannot modify a PublishedRequest once it is read-only."); - } + EnsureWriteable(); + _domain = value; } + } - 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 a value indicating whether the content request has a domain. + /// + public bool HasDomain => Domain != null; - /// - /// 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() + /// + /// Gets or sets the content request's culture. + /// + public CultureInfo Culture + { + get => _culture ?? Thread.CurrentThread.CurrentCulture; + set { EnsureWriteable(); - - // note: it can very well be null if the initial content was not found - _initialPublishedContent = _publishedContent; - IsInternalRedirectPublishedContent = false; + _culture = value; } + } - /// - /// 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 + // utility for ensuring it is ok to set some properties + public void EnsureWriteable() + { + if (_readonly) { - get { return _domain; } - set - { - EnsureWriteable(); - _domain = value; - } + 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 a value indicating whether the content request has a domain. - /// - public bool HasDomain => Domain != null; + /// + /// 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(); - /// - /// Gets or sets the content request's culture. - /// - public CultureInfo Culture + // 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 { - get { return _culture ?? Thread.CurrentThread.CurrentCulture; } - set - { - EnsureWriteable(); - _culture = value; - } + EnsureWriteable(); + _is404 = value; } + } - // note: do we want to have an ordered list of alternate cultures, - // to allow for fallbacks when doing dictionary lookup and such? - + /// + /// 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; + } - #region Status + /// + /// 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; + } - /// - /// 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; - } - } + /// + /// 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(); - /// - /// 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) + if (status < 300 || status > 308) { - EnsureWriteable(); - RedirectUrl = url; - IsRedirectPermanent = false; + throw new ArgumentOutOfRangeException(nameof(status), "Valid redirection status codes 300-308."); } - /// - /// 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; - } + RedirectUrl = url; + IsRedirectPermanent = status == 301 || status == 308; - /// - /// 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) + // default redirect statuses + if (status != 301 && status != 302) { - 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; + 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(); + /// + /// 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; - } + // .Status is deprecated + // .SubStatusCode is IIS 7+ internal, ignore + ResponseStatusCode = code; + ResponseStatusDescription = description; + } - #endregion + #endregion - #region Response Cache + #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 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 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 Dictionary(); + /// + /// Gets or sets a dictionary of Headers to append to the Response object. + /// + public Dictionary Headers { get; set; } = new(); - #endregion - } + #endregion } diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index 119f9980b4c5..5f195f78b534 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.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; - - /// - /// 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) - { - _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); - } - - /// - 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; - } - - private async Task TryRouteRequest(IPublishedRequestBuilder request) - { - FindDomain(request); - - if (request.IsRedirect()) - { - return request.Build(); - } - - if (request.HasPublishedContent()) - { - return request.Build(); - } + _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); + } - await FindPublishedContent(request); + /// + 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; + } - return request.Build(); + /// + public async Task RouteRequestAsync( + IPublishedRequestBuilder builder, + RouteRequestOptions options) + { + // outbound routing performs different/simpler logic + if (options.RouteDirection == RouteDirection.Outbound) + { + return await TryRouteRequest(builder); } - private void SetVariationContext(string? culture) + // find domain + if (builder.Domain == null) { - VariationContext? variationContext = _variationContextAccessor.VariationContext; - if (variationContext != null && variationContext.Culture == culture) - { - return; - } - - _variationContextAccessor.VariationContext = new VariationContext(culture); + FindDomain(builder); } - /// - public async Task RouteRequestAsync(IPublishedRequestBuilder builder, RouteRequestOptions options) - { - // outbound routing performs different/simpler logic - if (options.RouteDirection == RouteDirection.Outbound) - { - return await TryRouteRequest(builder); - } + await RouteRequestInternalAsync(builder); - // find domain - if (builder.Domain == null) - { - FindDomain(builder); - } + // complete the PCR and assign the remaining values + return BuildRequest(builder); + } - await RouteRequestInternalAsync(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); - // complete the PCR and assign the remaining values + // re-route + await RouteRequestInternalAsync(builder); + + // return if we are redirect + if (builder.IsRedirect()) + { 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; - } + // means the engine could not find a proper document to handle 404 + // restore the saved content so we know it exists + builder.SetPublishedContent(content); + } - // set the culture - SetVariationContext(builder.Culture); + if (!builder.HasDomain()) + { + FindDomain(builder); + } - var foundContentByFinders = false; + return BuildRequest(builder); + } - // 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); - } + /// + /// 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(); - // run the document finders - foundContentByFinders = await FindPublishedContent(builder); - } + if (!builder.HasPublishedContent()) + { + return result; + } - // if we are not a redirect - if (!builder.IsRedirect()) - { - // handle not-found, redirects, access... - await HandlePublishedContent(builder); + // set the culture -- again, 'cos it might have changed in the event handler + SetVariationContext(result.Culture); - // find a template - FindTemplate(builder, foundContentByFinders); + return result; + } - // handle umbracoRedirect - FollowExternalRedirect(builder); + private async Task TryRouteRequest(IPublishedRequestBuilder request) + { + FindDomain(request); - // handle wildcard domains - HandleWildcardDomains(builder); + if (request.IsRedirect()) + { + return request.Build(); + } - // set the culture -- again, 'cos it might have changed due to a finder or wildcard domain - SetVariationContext(builder.Culture); - } + if (request.HasPublishedContent()) + { + return request.Build(); + } - // 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); + await FindPublishedContent(request); - // 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 + return request.Build(); + } + + private void SetVariationContext(string? culture) + { + VariationContext? variationContext = _variationContextAccessor.VariationContext; + if (variationContext != null && variationContext.Culture == culture) + { + return; } - /// - /// 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) + _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()) { - IPublishedRequest result = builder.Build(); + return; + } + + // set the culture + SetVariationContext(builder.Culture); - if (!builder.HasPublishedContent()) + 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)) { - return result; + _logger.LogDebug("FindPublishedContentAndTemplate: Path={UriAbsolutePath}", builder.Uri.AbsolutePath); } - // set the culture -- again, 'cos it might have changed in the event handler - SetVariationContext(result.Culture); - - return result; + // run the document finders + foundContentByFinders = await FindPublishedContent(builder); } - /// - public async Task UpdateRequestAsync(IPublishedRequest request, IPublishedContent? publishedContent) + // if we are not a redirect + if (!builder.IsRedirect()) { - // store the original (if any) - IPublishedContent? content = request.PublishedContent; + // handle not-found, redirects, access... + await HandlePublishedContent(builder); - IPublishedRequestBuilder builder = new PublishedRequestBuilder(request.Uri, _fileService); + // find a template + FindTemplate(builder, foundContentByFinders); - // set to the new content (or null if specified) - builder.SetPublishedContent(publishedContent); + // handle umbracoRedirect + FollowExternalRedirect(builder); - // re-route - await RouteRequestInternalAsync(builder); + // handle wildcard domains + HandleWildcardDomains(builder); - // return if we are redirect - if (builder.IsRedirect()) - { - return BuildRequest(builder); - } + // set the culture -- again, 'cos it might have changed due to a finder or wildcard domain + SetVariationContext(builder.Culture); + } - // 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); - } + // 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); - if (!builder.HasDomain()) - { - FindDomain(builder); - } + // 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 + } - return BuildRequest(builder); + /// + /// 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); } - /// - /// 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) + 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) { - const string tracePrefix = "FindDomain: "; + // just get it from content cache - optimize there, not here + IPublishedContent? domainDocument = umbracoContext.PublishedSnapshot.Content?.GetById(domain.ContentId); - // note - we are not handling schemes nor ports here. - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + // not published - at all + if (domainDocument == null) { - _logger.LogDebug("{TracePrefix}Uri={RequestUri}", tracePrefix, request.Uri); + return false; } - 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); + // invariant - always published + if (!domainDocument.ContentType.VariesByCulture()) + { + return true; } - domains = domains?.Where(IsPublishedContentDomain).ToList(); + // variant, ensure that the culture corresponding to the domain's language is published + return domain.Culture is not null && domainDocument.Cultures.ContainsKey(domain.Culture); + } - var defaultCulture = domainsCache?.DefaultCulture; + domains = domains?.Where(IsPublishedContentDomain).ToList(); - // try to find a domain matching the current request - DomainAndUri? domainAndUri = DomainUtilities.SelectDomain(domains, request.Uri, defaultCulture: defaultCulture); + var defaultCulture = domainsCache?.DefaultCulture; - // handle domain - always has a contentId and a culture - if (domainAndUri != null) + // 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)) { - // 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; - // } + _logger.LogDebug( + "{TracePrefix}Matches domain={Domain}, rootId={RootContentId}, culture={Culture}", + tracePrefix, + domainAndUri.Name, + domainAndUri.ContentId, + domainAndUri.Culture); } - 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)) + 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}Culture={CultureName}", tracePrefix, request.Culture); + _logger.LogDebug("{TracePrefix}Matches no domain", tracePrefix); } - return request.Domain != null; + request.SetCulture(defaultCulture ?? CultureInfo.CurrentUICulture.Name); } - /// - /// Looks for wildcard domains in the path and updates Culture accordingly. - /// - internal void HandleWildcardDomains(IPublishedRequestBuilder request) + if (_logger.IsEnabled(LogLevel.Debug)) { - const string tracePrefix = "HandleWildcardDomains: "; + _logger.LogDebug("{TracePrefix}Culture={CultureName}", tracePrefix, request.Culture); + } - if (request.PublishedContent == null) - { - return; - } + return request.Domain != null; + } - 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); + /// + /// Looks for wildcard domains in the path and updates Culture accordingly. + /// + internal void HandleWildcardDomains(IPublishedRequestBuilder request) + { + const string tracePrefix = "HandleWildcardDomains: "; - // 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); - } - } + if (request.PublishedContent == null) + { + return; } - internal bool FindTemplateRenderingEngineInDirectory(DirectoryInfo directory, string alias, string[] extensions) + var nodePath = request.PublishedContent.Path; + if (_logger.IsEnabled(LogLevel.Debug)) { - if (directory == null || directory.Exists == false) + _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)) { - return false; + _logger.LogDebug( + "{TracePrefix}Got domain on node {DomainContentId}, set culture to {CultureName}", + tracePrefix, + domain.ContentId, + request.Culture); } - - var pos = alias.IndexOf('/'); - if (pos > 0) + } + else + { + if (_logger.IsEnabled(LogLevel.Debug)) { - // recurse - DirectoryInfo? subdir = directory.GetDirectories(alias.Substring(0, pos)).FirstOrDefault(); - alias = alias.Substring(pos + 1); - return subdir != null && FindTemplateRenderingEngineInDirectory(subdir, alias, extensions); + _logger.LogDebug("{TracePrefix}No match.", tracePrefix); } + } + } - // look here - return directory.GetFiles().Any(f => extensions.Any(e => f.Name.InvariantEquals(alias + e))); + internal bool FindTemplateRenderingEngineInDirectory(DirectoryInfo? directory, string alias, string[] extensions) + { + if (directory == null || directory.Exists == false) + { + return false; } - /// - /// Tries to find the document matching the request, by running the IPublishedContentFinder instances. - /// - /// There is no finder collection. - internal async Task FindPublishedContent(IPublishedRequestBuilder request) + var pos = alias.IndexOf('/'); + if (pos > 0) { - const string tracePrefix = "FindPublishedContent: "; + // 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 + // 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)) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - profilingScope = _profilingLogger.DebugDuration( + profilingScope = _profilingLogger.DebugDuration( $"{tracePrefix}Begin finders", $"{tracePrefix}End finders"); - } + } - // iterate but return on first one that finds it - var found = false; - foreach (var contentFinder in _contentFinders) + // iterate but return on first one that finds it + var found = false; + foreach (IContentFinder contentFinder in _contentFinders) + { + if (_logger.IsEnabled(LogLevel.Debug)) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("Finder {ContentFinderType}", contentFinder.GetType().FullName); - } - found = await contentFinder.TryFindContent(request); - if (found) - { - break; - } + _logger.LogDebug("Finder {ContentFinderType}", contentFinder.GetType().FullName); } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + found = await contentFinder.TryFindContent(request); + if (found) { - _logger.LogDebug( + break; + } + } + + 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 @@ internal async Task FindPublishedContent(IPublishedRequestBuilder request) request.HasDomain() ? request.Domain?.ToString() : "NULL", request.Culture ?? "NULL", request.ResponseStatusCode); - } - - return found; } - finally + + return found; + } + finally + { + 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)) { - profilingScope?.Dispose(); + _logger.LogDebug("HandlePublishedContent: Loop {LoopCounter}", i); } - } - /// - /// 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 + // handle not found + if (request.PublishedContent == null) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + 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"); - } + break; } - // 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) + if (_logger.IsEnabled(LogLevel.Debug)) { - break; + _logger.LogDebug("HandlePublishedContent: Found a document"); } + } - // 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) + // 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) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("HandlePublishedContent: Looks like we are running into an infinite loop, abort"); - } - request.SetPublishedContent(null); } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + + // we're running out of control + if (j == maxLoop) { - _logger.LogDebug("HandlePublishedContent: End"); + 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); - /// - /// 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 (i == maxLoop || j == maxLoop) { - if (request.PublishedContent == null) + if (_logger.IsEnabled(LogLevel.Debug)) { - throw new InvalidOperationException("There is no PublishedContent."); + _logger.LogDebug("HandlePublishedContent: Looks like we are running into an infinite loop, abort"); } - // don't try to find a redirect if the property doesn't exist - if (request.PublishedContent.HasProperty(Constants.Conventions.Content.InternalRedirectId) == false) - { - return false; - } + request.SetPublishedContent(null); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("HandlePublishedContent: End"); + } + } - 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(); + /// + /// 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."); + } - if (internalRedirectId > 0) + // 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 legacy integer ID + // try and get the redirect node from a UDI Guid 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); - } + internalRedirectNode = umbracoContext.Content?.GetById(udiInternalRedirectId.Guid); } + } - if (valid == false) + if (valid == false) + { + // bad redirect - log and display the current page (legacy behavior) + if (_logger.IsEnabled(LogLevel.Debug)) { - // 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 (internalRedirectNode == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug( + _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) + } + else if (internalRedirectId == request.PublishedContent.Id) + { + // redirect to self + if (_logger.IsEnabled(LogLevel.Debug)) { - // redirect to self - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FollowInternalRedirects: Redirecting to self, ignore"); - } + _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 + } + else + { + // save since it will be cleared + ITemplate? template = request.Template; - // must restore the template if it's an internal redirect & the config option is set - if (request.IsInternalRedirect && _webRoutingSettings.InternalRedirectPreservesTemplate) - { - // restore - request.SetTemplate(template); - } + request.SetInternalRedirect(internalRedirectNode); // don't use .PublishedContent here - redirect = true; - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FollowInternalRedirects: Redirecting to id={InternalRedirectId}", internalRedirectId); - } + // must restore the template if it's an internal redirect & the config option is set + if (request.IsInternalRedirect && _webRoutingSettings.InternalRedirectPreservesTemplate) + { + // restore + request.SetTemplate(template); } - return redirect; + redirect = true; + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("FollowInternalRedirects: Redirecting to id={InternalRedirectId}", internalRedirectId); + } } - /// - /// 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) + 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) { - // 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; - } + 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); + // 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; + var altTemplate = useAltTemplate + ? _requestAccessor.GetRequestValue(Constants.Conventions.Url.AltTemplate) + : null; - if (string.IsNullOrWhiteSpace(altTemplate)) + 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()) { - // 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; + _logger.LogDebug("FindTemplate: Has a template already, and no alternate template."); } - // 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 + 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(LogLevel.Debug)) { - _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()) - { - 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)) + // ignore if the alias does not match - just trace + if (request.HasTemplate()) + { + if (_logger.IsEnabled(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) - { - 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 (template != null) + { + request.SetTemplate(template); + if (_logger.IsEnabled(LogLevel.Debug)) { - 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()) + else { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + _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)) { - _logger.LogDebug("FindTemplate: No template was found."); + _logger.LogDebug( + "FindTemplate: Running with template id={TemplateId} alias={TemplateAlias}", + template?.Id, + template?.Alias); } - - // 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 (!request.HasTemplate()) { - 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)) + if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("GetTemplateModel: Get template id={TemplateId}", templateId); + _logger.LogDebug("FindTemplate: No template was found."); } - if (templateId == null) - { - throw new InvalidOperationException("The template is not set, the page cannot render."); - } + // 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 + } + } - 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)) + private ITemplate? GetTemplate(int? templateId) + { + if (templateId.HasValue == false || templateId.Value == default) + { + if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("GetTemplateModel: Got template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); + _logger.LogDebug("GetTemplateModel: No template."); } - return template; + + return null; } - /// - /// 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 (_logger.IsEnabled(LogLevel.Debug)) { - if (request.PublishedContent == null) - { - return; - } + _logger.LogDebug("GetTemplateModel: Get template id={TemplateId}", templateId); + } - // don't try to find a redirect if the property doesn't exist - if (request.PublishedContent.HasProperty(Constants.Conventions.Content.Redirect) == false) - { - return; - } + if (templateId == null) + { + throw new InvalidOperationException("The template is not set, the page cannot render."); + } - 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); - } - } + 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; + } - if (redirectUrl != "#") + /// + /// 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) { - request.SetRedirect(redirectUrl); + 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 33dad7b08107..7ba637c28867 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 97792ebad38b..960bf4bd3610 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 a74d4532e1a7..b8ae10c3aa84 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 @@ public void BindSites(params string[] keys) #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 @@ public void BindSites(params string[] keys) } /// - 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 @@ public virtual IEnumerable MapDomains(IReadOnlyCollection d != mainDomain); } } @@ -368,16 +361,19 @@ public virtual IEnumerable MapDomains(IReadOnlyCollection 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 5d298b811aae..fe1e83d2545f 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; - - /// - /// Initializes a new instance of the class. - /// - public UmbracoRequestPaths(IOptions globalSettings, IHostingEnvironment hostingEnvironment) - { - var applicationPath = hostingEnvironment.ApplicationVirtualPath; - _appPath = applicationPath.TrimStart(Constants.CharArrays.ForwardSlash); + 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); - } - - /// - /// 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('/'); + _backOfficePath = globalSettings.Value.GetBackOfficePath(hostingEnvironment) + .EnsureStartsWith('/').TrimStart(_appPath.EnsureStartsWith('/')).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; - } + _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); + } - // if its the normal /umbraco path - if (_defaultUmbPaths.Any(x => urlPath.InvariantEquals(x))) - { - return true; - } + /// + /// 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 for special back office paths - if (urlPath.InvariantStartsWith(_backOfficeMvcPath) - || urlPath.InvariantStartsWith(_previewMvcPath)) - { - return true; - } + // check if this is in the umbraco back office + var isUmbracoPath = urlPath.InvariantStartsWith(_backOfficePath); - // check for special front-end paths - if (urlPath.InvariantStartsWith(_surfaceMvcPath) - || urlPath.InvariantStartsWith(_apiMvcPath)) - { - return false; - } + // if not, then def not back office + if (isUmbracoPath == false) + { + 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 the normal /umbraco path + if (_defaultUmbPaths.Any(x => urlPath.InvariantEquals(x))) + { + return true; + } - // if its anything else we can assume it's back office + // check for special back office paths + if (urlPath.InvariantStartsWith(_backOfficeMvcPath) + || urlPath.InvariantStartsWith(_previewMvcPath)) + { return true; } - /// - /// Checks if the current uri is an install request - /// - public bool IsInstallerRequest(string absPath) => absPath.InvariantStartsWith(_installPath); + // check for special front-end paths + if (urlPath.InvariantStartsWith(_surfaceMvcPath) + || urlPath.InvariantStartsWith(_apiMvcPath)) + { + return false; + } - /// - /// Rudimentary check to see if it's not a server side request - /// - public bool IsClientSideRequest(string absPath) + // 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) { - var ext = Path.GetExtension(absPath); - return !ext.IsNullOrWhiteSpace(); + 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 d41c7ad7c388..67690e11e8b8 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 b973bdd068b6..fb59ada249be 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; + if (hostingEnvironment is null) + { + throw new ArgumentNullException(nameof(hostingEnvironment)); + } - public UriUtility(IHostingEnvironment hostingEnvironment) + 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 == "/") { - if (hostingEnvironment is null) throw new ArgumentNullException(nameof(hostingEnvironment)); - ResetAppDomainAppVirtualPath(hostingEnvironment); + _appPathPrefix = string.Empty; } + } - // internal for unit testing only - internal void SetAppDomainAppVirtualPath(string appPath) + 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] == '/')) { - _appPath = appPath ?? "/"; - _appPathPrefix = _appPath; - if (_appPathPrefix == "/") - _appPathPrefix = String.Empty; + 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) - { - if (_appPathPrefix is not null && virtualPath.InvariantStartsWith(_appPathPrefix) - && (virtualPath.Length == _appPathPrefix.Length || virtualPath[_appPathPrefix.Length] == '/')) - { - virtualPath = virtualPath.Substring(_appPathPrefix.Length); - } + path = ToAbsolute(path); - if (virtualPath.Length == 0) - { - virtualPath = "/"; - } + return uri.Rewrite(path); + } - return virtualPath; - } + // 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 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(); + // 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? - if (path != "/" && requestConfig.AddTrailingSlash) - path = path.EnsureEndsWith("/"); + // 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 = ToAbsolute(path); + path = path.ToLower(); + path = ToAppRelative(path); // strip vdir if any - return uri.Rewrite(path); + if (path != "/") + { + path = path.TrimEnd(Constants.CharArrays.ForwardSlash); } - // maps a media umbraco uri to a public uri - // ie with virtual directory - that is all for media - public Uri MediaUriFromUmbraco(Uri uri) + 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) { - var path = uri.GetSafeAbsolutePath(); - path = ToAbsolute(path); - return uri.Rewrite(path); + throw new ArgumentNullException("relativeUrl"); } - // maps a public uri to an internal umbraco uri - // ie no virtual directory, no .aspx, lowercase... - public Uri UriToUmbraco(Uri uri) + if (relativeUrl.Length == 0 || relativeUrl[0] == '/' || relativeUrl[0] == '\\') { - // 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 + return relativeUrl; + } - if (path != "/") + var idxOfScheme = relativeUrl.IndexOf(@"://", StringComparison.Ordinal); + if (idxOfScheme != -1) + { + var idxOfQM = relativeUrl.IndexOf('?'); + if (idxOfQM == -1 || idxOfQM > idxOfScheme) { - path = path.TrimEnd(Constants.CharArrays.ForwardSlash); + return relativeUrl; } - - 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) + var sbUrl = new StringBuilder(); + sbUrl.Append(_appPathPrefix); + if (sbUrl.Length == 0 || sbUrl[^1] != '/') { - 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; - } + sbUrl.Append('/'); + } - StringBuilder sbUrl = new StringBuilder(); - sbUrl.Append(_appPathPrefix); - if (sbUrl.Length == 0 || sbUrl[sbUrl.Length - 1] != '/') sbUrl.Append('/'); + // 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; + } - // 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] == '\\')) - { - relativeUrl = relativeUrl.Substring(2); - foundSlash = true; - } - else foundSlash = false; - foreach (char c in relativeUrl) + foreach (var c in relativeUrl) + { + if (!foundQM) { - if (!foundQM) + if (c == '?') + { + foundQM = true; + } + else { - if (c == '?') 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(); + } - /// - /// 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) + #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) + { + 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 3a5c27772522..f5b208fb73e8 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) { + if (string.IsNullOrWhiteSpace(text)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(text)); + } - /// - /// Creates a instance representing a true URL. - /// - public static UrlInfo Url(string text, string? culture = null) => new UrlInfo(text, true, culture); + IsUrl = isUrl; + Text = text; + Culture = culture; + } - /// - /// Creates a instance representing a message. - /// - public static UrlInfo Message(string text, string? culture = null) => new UrlInfo(text, false, culture); + /// + /// Gets the culture. + /// + [DataMember(Name = "culture")] + public string? Culture { get; } - /// - /// Initializes a new instance of the class. - /// - public UrlInfo(string text, bool isUrl, string? culture) - { - if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(text)); - 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 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); - /// - /// Gets the culture. - /// - [DataMember(Name = "culture")] - public string? Culture { get; } - - /// - /// 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 text, which is either the URL, or a message. - /// - [DataMember(Name = "text")] - public string Text { 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) + /// + /// 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 e19e38889345..97385a144b8a 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 @@ public UrlProvider(IUmbracoContextAccessor umbracoContextAccessor, IOptions 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 @@ public string GetUrlFromRoute(int id, string? route, string? culture) /// public IEnumerable GetOtherUrls(int id) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); return GetOtherUrls(id, umbracoContext.CleanedUmbracoUrl); } @@ -204,18 +205,24 @@ public string GetMediaUrl(Guid id, UrlMode mode = UrlMode.Default, string? cultu /// 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 @@ public string GetMediaUrl(IPublishedContent? content, UrlMode mode = UrlMode.Def 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 c17417c83cad..0acb75264d10 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 ca6f703c8b9f..fe975272dde3 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 5f28bee2b704..8e2a577f3a47 100644 --- a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs @@ -6,250 +6,256 @@ 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; - } + 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); - } + // 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)) + // 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) { - // 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); - } + result.AddRange(urlGroup.DistinctBy(x => x.Text, StringComparer.OrdinalIgnoreCase).OrderBy(x => x.Text) + .ThenBy(x => x.Culture)); } - - // 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)) + else { - // avoid duplicates - if (urls.Add(otherUrl)) - { - result.Add(otherUrl); - } + result.AddRange(urlGroup); } - - 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) + // 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)) { - var result = new List(); - - foreach (var culture in cultures) + // avoid duplicates + if (urls.Add(otherUrl)) { - // 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; - } + result.Add(otherUrl); } - - return result; } - private static UrlInfo HandleCouldNotGetUrl(IContent content, string culture, IContentService contentService, ILocalizedTextService textService) + 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) { - // 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 + // if content is variant, and culture is not published, skip + if (content.ContentType.VariesByCulture() && !content.IsCulturePublished(culture)) { - parent = parent.ParentId > 0 ? contentService.GetParent(parent) : null; + continue; } - while (parent != null && parent.Published && (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture))); - if (parent == null) + // 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) { - // oops, internal error - return UrlInfo.Message(textService.Localize("content", "parentNotPublishedAnomaly"), culture); + logger.LogError(ex, "GetUrl exception."); + url = "#ex"; } - if (!parent.Published) + switch (url) { - // totally not published - return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] { parent.Name }), culture); + // 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; } + } - // culture not published - return UrlInfo.Message(textService.Localize("content", "parentCultureNotPublished", new[] { parent.Name }), culture); + return result; + } + + 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))); - private static async Task> DetectCollisionAsync( - ILogger logger, - IContent content, - string url, - string culture, - IUmbracoContext umbracoContext, - IPublishedRouter publishedRouter, - ILocalizedTextService textService, - IVariationContextAccessor variationContextAccessor, - UriUtility uriUtility) + if (parent == null) { - // 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); - } + // oops, internal error + return UrlInfo.Message(textService.Localize("content", "parentNotPublishedAnomaly"), culture); + } - uri = uriUtility.UriToUmbraco(uri); - IPublishedRequestBuilder builder = await publishedRouter.CreateRequestAsync(uri); - IPublishedRequest pcr = await publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound)); + if (!parent.Published) + { + // totally not published + return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] { parent.Name }), culture); + } - 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); + // culture not published + return UrlInfo.Message( + textService.Localize("content", "parentCultureNotPublished", new[] { parent.Name }), + culture); + } - var urlInfo = UrlInfo.Message(textService.Localize("content", "routeErrorCannotRoute"), culture); - return Attempt.Succeed(urlInfo); - } + 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); + } - if (pcr.IgnorePublishedContentCollisions) - { - return Attempt.Fail(); - } + 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) + if (pcr.PublishedContent?.Id != content.Id) + { + IPublishedContent? o = pcr.PublishedContent; + var l = new List(); + while (o != null) { - 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); + l.Add(o.Name(variationContextAccessor)!); + o = o.Parent; } - // no collision - return Attempt.Fail(); + 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 a4da94ac7988..7ecafff8a324 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 = '/'; + if (paths == null) + { + throw new ArgumentNullException(nameof(paths)); + } + + if (paths.Length == 0) + { + return string.Empty; + } + + var sb = new StringBuilder(); - public static string Combine(params string[]? paths) + for (var index = 0; index < paths.Length; index++) { - if (paths == null) + 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) { - throw new ArgumentNullException(nameof(paths)); + start = 1; } - if (paths.Length == 0) + // always trim end + if (path[^1] == PathSeparator) { - return string.Empty; + count = path.Length - 1; } - var sb = new StringBuilder(); + sb.Append(path, start, count - start); - for (var index = 0; index < paths.Length; index++) + if (!isLast) { - 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); - } + sb.Append(PathSeparator); } - - return sb.ToString(); } + + return sb.ToString(); } } diff --git a/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs b/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs index 6c45e4d96915..8d7ec082be1d 100644 --- a/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs +++ b/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs @@ -5,31 +5,29 @@ using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Runtime -{ - public class EssentialDirectoryCreator : INotificationHandler - { - private readonly IIOHelper _ioHelper; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly GlobalSettings _globalSettings; +namespace Umbraco.Cms.Core.Runtime; - public EssentialDirectoryCreator(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, IOptions globalSettings) - { - _ioHelper = ioHelper; - _hostingEnvironment = hostingEnvironment; - _globalSettings = globalSettings.Value; - } +public class EssentialDirectoryCreator : INotificationHandler +{ + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IIOHelper _ioHelper; - 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 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)); } } diff --git a/src/Umbraco.Core/Runtime/IMainDom.cs b/src/Umbraco.Core/Runtime/IMainDom.cs index 65c64857b32d..59278e161c6c 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 5b8fb819e658..cbfbffac4cb9 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 b0b3394a01ec..7e58fa6533b2 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 48ea6a5a48e7..b5c43cde4acf 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 0198382b2a8d..83736914a2ba 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 @@ public class MainDom : IMainDom, IRegisteredObject, IDisposable 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 @@ public class MainDom : IMainDom, IRegisteredObject, IDisposable 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 @@ private void OnSignal(string source) 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 @@ private bool Acquire() { // 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 @@ private bool Acquire() } 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 @@ void IRegisteredObject.Stop(bool immediate) // 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 5d2248906ee7..cdfd7b9305fa 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 SystemLock _systemLock; + 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 ILogger _logger; - private IDisposable? _lockRelease; + // 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) + 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 + var eventName = "UMBRACO-" + mainDomId + "-MAINDOM-EVT"; + _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + _logger = logger; + } - // signal, then wait for the lock, then make sure the event is - // reset (maybe there was noone listening..) - _signal.Set(); + // 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(); - // if more than 1 instance reach that point, one will get the lock - // and the other one will timeout, which is accepted + 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 - // 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, then wait for the lock, then make sure the event is + // reset (maybe there was noone listening..) + _signal.Set(); - _signal.Reset(); - } + // 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 + #region IDisposable Support - protected virtual void Dispose(bool disposing) + private bool disposedValue; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) { - if (!disposedValue) + if (disposing) { - if (disposing) - { - _lockRelease?.Dispose(); - _signal.Close(); - _signal.Dispose(); - } - - disposedValue = true; + _lockRelease?.Dispose(); + _signal.Close(); + _signal.Dispose(); } - } - // 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); + disposedValue = true; } - #endregion } + + // 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 d6687d4628f4..5b726045a93f 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 94192c83b2c6..76f47e8a1728 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 8bb85ca29d90..ef3cf91c4c42 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 d4fe496d38ff..792673453f44 100644 --- a/src/Umbraco.Core/Scoping/ICoreScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/ICoreScopeProvider.cs @@ -2,49 +2,50 @@ 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 9d0bc9ceef5a..1942ecdc43e2 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 7f1302911a16..26f17b31b03e 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 78c50b628f29..75361726f374 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 828adea2952f..f8d46747b1ee 100644 --- a/src/Umbraco.Core/Sections/ContentSection.cs +++ b/src/Umbraco.Core/Sections/ContentSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +/// +/// Defines the back office content section +/// +public class ContentSection : ISection { - /// - /// Defines the back office content section - /// - public class ContentSection : ISection - { - /// - public string Alias => Constants.Applications.Content; + /// + public string Alias => Constants.Applications.Content; - /// - public string Name => "Content"; - } + /// + public string Name => "Content"; } diff --git a/src/Umbraco.Core/Sections/FormsSection.cs b/src/Umbraco.Core/Sections/FormsSection.cs index e0fd1085eed2..3ac36e47326e 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 bbd380f57ea8..565955dfe9ac 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 8732556a2873..f5fd0a79b78e 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 1edbf1260411..a2e98ac871e1 100644 --- a/src/Umbraco.Core/Sections/MembersSection.cs +++ b/src/Umbraco.Core/Sections/MembersSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +/// +/// Defines the back office members section +/// +public class MembersSection : ISection { - /// - /// Defines the back office members section - /// - public class MembersSection : ISection - { - /// - public string Alias => Constants.Applications.Members; + /// + public string Alias => Constants.Applications.Members; - /// - public string Name => "Members"; - } + /// + public string Name => "Members"; } diff --git a/src/Umbraco.Core/Sections/PackagesSection.cs b/src/Umbraco.Core/Sections/PackagesSection.cs index 4852c11397cb..d65acfccec8f 100644 --- a/src/Umbraco.Core/Sections/PackagesSection.cs +++ b/src/Umbraco.Core/Sections/PackagesSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +/// +/// Defines the back office packages section +/// +public class PackagesSection : ISection { - /// - /// Defines the back office packages section - /// - public class PackagesSection : ISection - { - /// - public string Alias => Constants.Applications.Packages; + /// + public string Alias => Constants.Applications.Packages; - /// - public string Name => "Packages"; - } + /// + public string Name => "Packages"; } diff --git a/src/Umbraco.Core/Sections/SectionCollection.cs b/src/Umbraco.Core/Sections/SectionCollection.cs index 5ff0157d149c..83169a390dca 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 219d6342617b..7644b1cc8ced 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 SectionCollectionBuilder This => this; - 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(); + 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(); - 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 bc0a43cae128..3fe825c70df3 100644 --- a/src/Umbraco.Core/Sections/SettingsSection.cs +++ b/src/Umbraco.Core/Sections/SettingsSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +/// +/// Defines the back office settings section +/// +public class SettingsSection : ISection { - /// - /// Defines the back office settings section - /// - public class SettingsSection : ISection - { - /// - public string Alias => Constants.Applications.Settings; + /// + public string Alias => Constants.Applications.Settings; - /// - public string Name => "Settings"; - } + /// + public string Name => "Settings"; } diff --git a/src/Umbraco.Core/Sections/TranslationSection.cs b/src/Umbraco.Core/Sections/TranslationSection.cs index d739757e93d1..d11391c8111f 100644 --- a/src/Umbraco.Core/Sections/TranslationSection.cs +++ b/src/Umbraco.Core/Sections/TranslationSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +/// +/// Defines the back office translation section +/// +public class TranslationSection : ISection { - /// - /// Defines the back office translation section - /// - public class TranslationSection : ISection - { - /// - public string Alias => Constants.Applications.Translation; + /// + public string Alias => Constants.Applications.Translation; - /// - public string Name => "Translation"; - } + /// + public string Name => "Translation"; } diff --git a/src/Umbraco.Core/Sections/UsersSection.cs b/src/Umbraco.Core/Sections/UsersSection.cs index 6969e9be3de4..cea5047c8104 100644 --- a/src/Umbraco.Core/Sections/UsersSection.cs +++ b/src/Umbraco.Core/Sections/UsersSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +/// +/// Defines the back office users section +/// +public class UsersSection : ISection { - /// - /// Defines the back office users section - /// - public class UsersSection : ISection - { - /// - public string Alias => Constants.Applications.Users; + /// + public string Alias => Constants.Applications.Users; - /// - public string Name => "Users"; - } + /// + public string Name => "Users"; } diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 187d44b05d6b..2b8294e8dbc2 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; - } + Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = culture; } + } - public static CultureInfo? GetCulture(this IIdentity identity) + public static CultureInfo? GetCulture(this IIdentity identity) + { + if (identity is ClaimsIdentity umbIdentity && umbIdentity.VerifyBackOfficeIdentity(out _) && + umbIdentity.IsAuthenticated && umbIdentity.GetCultureString() is not null) { - if (identity is ClaimsIdentity umbIdentity && umbIdentity.VerifyBackOfficeIdentity(out _) && umbIdentity.IsAuthenticated && umbIdentity.GetCultureString() is not null) - { - return CultureInfo.GetCultureInfo(umbIdentity.GetCultureString()!); - } - - return 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 c79fa8742921..cece444588c6 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 e4eacaf9d612..913f4c6dde88 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 a59c1fb435b3..5466642a1406 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 b9912a391180..4cb9e20dac42 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) + + 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)) { - //If it's already a UmbracoBackOfficeIdentity - if (principal.Identity is ClaimsIdentity claimsIdentity - && claimsIdentity.IsBackOfficeAuthenticationType() - && claimsIdentity.VerifyBackOfficeIdentity(out var backOfficeIdentity)) - { - return 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 ) + // 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; + // Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd + if (principal.Identity is ClaimsIdentity claimsIdentity2 + && claimsIdentity2.VerifyBackOfficeIdentity(out backOfficeIdentity)) + { + return backOfficeIdentity; } - /// - /// 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); + 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) + /// + /// 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) { - var claimsPrincipal = user as ClaimsPrincipal; - if (claimsPrincipal == null) return 0; + return 0; + } - var ticketExpires = claimsPrincipal.FindFirst(Constants.Security.TicketExpiresClaimType)?.Value; - if (ticketExpires.IsNullOrWhiteSpace()) return 0; + var ticketExpires = claimsPrincipal.FindFirst(Constants.Security.TicketExpiresClaimType)?.Value; + if (ticketExpires.IsNullOrWhiteSpace()) + { + return 0; + } - var utcExpired = DateTimeOffset.Parse(ticketExpires!, null, DateTimeStyles.RoundtripKind); + var utcExpired = DateTimeOffset.Parse(ticketExpires!, null, DateTimeStyles.RoundtripKind); - var secondsRemaining = utcExpired.Subtract(now).TotalSeconds; - return secondsRemaining; - } + 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 73f9f4ccef49..db27d100c6b9 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 + { + Granted, + Denied, + NotFound, + } + + private readonly IContentService _contentService; + private readonly IEntityService _entityService; + private readonly IUserService _userService; + + public ContentPermissions( + IUserService userService, + IContentService contentService, + IEntityService entityService, + AppCaches appCaches) { - private readonly IUserService _userService; - private readonly IContentService _contentService; - private readonly IEntityService _entityService; - private readonly AppCaches _appCaches; + _userService = userService; + _contentService = contentService; + _entityService = entityService; + _appCaches = appCaches; + } - public enum ContentAccess + 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; + return false; } - public ContentAccess CheckPermissions( - IContent content, - IUser user, - char permissionToCheck) => CheckPermissions(content, user, new[] { permissionToCheck }); - - public ContentAccess CheckPermissions( - IContent? content, - IUser? user, - IReadOnlyList permissionsToCheck) + // check for root access + if (startNodeIds.Contains(Constants.System.Root)) { - if (user == null) throw new ArgumentNullException(nameof(user)); + return true; + } - if (content == null) return ContentAccess.NotFound; + var formattedPath = string.Concat(",", path, ","); - var hasPathAccess = user.HasPathAccess(content, _entityService, _appCaches); + // 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; + } - if (hasPathAccess == false) - return ContentAccess.Denied; + // check for a start node in the path + return startNodeIds.Any(x => + formattedPath.Contains(string.Concat(",", x.ToString(CultureInfo.InvariantCulture), ","))); + } - if (permissionsToCheck == null || permissionsToCheck.Count == 0) - return ContentAccess.Granted; + public ContentAccess CheckPermissions( + IContent content, + IUser user, + char permissionToCheck) => CheckPermissions(content, user, new[] { permissionToCheck }); - //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(content.Path, user, permissionsToCheck) - ? ContentAccess.Granted - : ContentAccess.Denied; + public ContentAccess CheckPermissions( + IContent? content, + IUser? user, + IReadOnlyList permissionsToCheck) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (content == null) + { + return ContentAccess.NotFound; } - public ContentAccess CheckPermissions( - IUmbracoEntity entity, - IUser? user, - char permissionToCheck) => CheckPermissions(entity, user, new[] { permissionToCheck }); + var hasPathAccess = user.HasPathAccess(content, _entityService, _appCaches); - public ContentAccess CheckPermissions( - IUmbracoEntity entity, - IUser? user, - IReadOnlyList permissionsToCheck) + if (hasPathAccess == false) { - if (user == null) throw new ArgumentNullException(nameof(user)); + 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; + } - if (entity == null) return ContentAccess.NotFound; + public ContentAccess CheckPermissions( + IUmbracoEntity entity, + IUser? user, + char permissionToCheck) => CheckPermissions(entity, user, new[] { permissionToCheck }); - var hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches); + public ContentAccess CheckPermissions( + IUmbracoEntity entity, + IUser? user, + IReadOnlyList permissionsToCheck) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } - if (hasPathAccess == false) - return ContentAccess.Denied; + if (entity == null) + { + return ContentAccess.NotFound; + } - if (permissionsToCheck == null || permissionsToCheck.Count == 0) - return ContentAccess.Granted; + var hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches); - //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(entity.Path, user, permissionsToCheck) - ? ContentAccess.Granted - : ContentAccess.Denied; + if (hasPathAccess == false) + { + return 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 (permissionsToCheck == null || permissionsToCheck.Count == 0) { - if (user == null) throw new ArgumentNullException(nameof(user)); + return ContentAccess.Granted; + } - if (permissionsToCheck == null) - { - permissionsToCheck = Array.Empty(); - } + // get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(entity.Path, user, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } - 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(); - } + /// + /// 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)); + } - bool? hasPathAccess = null; - contentItem = null; + if (permissionsToCheck == null) + { + permissionsToCheck = Array.Empty(); + } - if (nodeId == Constants.System.Root) - hasPathAccess = user.HasContentRootAccess(_entityService, _appCaches); - else if (nodeId == Constants.System.RecycleBinContent) - hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches); + bool? hasPathAccess = null; + entity = null; - if (hasPathAccess.HasValue) - return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; + if (nodeId == Constants.System.Root) + { + hasPathAccess = user.HasContentRootAccess(_entityService, _appCaches); + } + else if (nodeId == Constants.System.RecycleBinContent) + { + hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches); + } - contentItem = _contentService.GetById(nodeId); - if (contentItem == null) return ContentAccess.NotFound; - hasPathAccess = user.HasPathAccess(contentItem, _entityService, _appCaches); + if (hasPathAccess.HasValue) + { + return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; + } - if (hasPathAccess == false) - return ContentAccess.Denied; + entity = _entityService.Get(nodeId, UmbracoObjectTypes.Document); + if (entity == null) + { + return ContentAccess.NotFound; + } - if (permissionsToCheck == null || permissionsToCheck.Count == 0) - return ContentAccess.Granted; + hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches); - //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(contentItem.Path, user, permissionsToCheck) - ? ContentAccess.Granted - : ContentAccess.Denied; + if (hasPathAccess == false) + { + return ContentAccess.Denied; } - private bool CheckPermissionsPath(string? path, IUser user, IReadOnlyList? permissionsToCheck = null) + if (permissionsToCheck == null || permissionsToCheck.Count == 0) { - if (permissionsToCheck == null) - { - permissionsToCheck = Array.Empty(); - } + return ContentAccess.Granted; + } - //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); + // get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(entity.Path, user, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } - var allowed = true; - foreach (var p in permissionsToCheck) - { - if (permission == null - || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) - { - allowed = false; - } - } - return allowed; + /// + /// 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)); } - public static bool HasPathAccess(string? path, int[]? startNodeIds, int recycleBinId) + if (permissionsToCheck == null) { - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + permissionsToCheck = Array.Empty(); + } - // check for no access - if (startNodeIds is null || startNodeIds.Length == 0) - return false; + bool? hasPathAccess = null; + contentItem = null; - // check for root access - if (startNodeIds.Contains(Constants.System.Root)) - return true; + if (nodeId == Constants.System.Root) + { + hasPathAccess = user.HasContentRootAccess(_entityService, _appCaches); + } + else if (nodeId == Constants.System.RecycleBinContent) + { + hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches); + } - var formattedPath = string.Concat(",", path, ","); + if (hasPathAccess.HasValue) + { + return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; + } + + contentItem = _contentService.GetById(nodeId); + if (contentItem == null) + { + return ContentAccess.NotFound; + } - // 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; + hasPathAccess = user.HasPathAccess(contentItem, _entityService, _appCaches); - // check for a start node in the path - return startNodeIds.Any(x => formattedPath.Contains(string.Concat(",", x.ToString(CultureInfo.InvariantCulture), ","))); + if (hasPathAccess == false) + { + return ContentAccess.Denied; } - public static bool IsInBranchOfStartNode(string path, int[]? startNodeIds, string[]? startNodePaths, out bool hasPathAccess) + if (permissionsToCheck == null || permissionsToCheck.Count == 0) { - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + return ContentAccess.Granted; + } + + // get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(contentItem.Path, user, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } - hasPathAccess = false; + private bool CheckPermissionsPath(string? path, IUser user, IReadOnlyList? permissionsToCheck = null) + { + if (permissionsToCheck == null) + { + permissionsToCheck = Array.Empty(); + } - // check for no access - if (startNodeIds?.Length == 0) - return false; + // 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); - // check for root access - if (startNodeIds?.Contains(Constants.System.Root) ?? false) + var allowed = true; + foreach (var p in permissionsToCheck) + { + if (permission == null + || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) { - hasPathAccess = true; - return true; + allowed = false; } + } - //is it self? - var self = startNodePaths?.Any(x => x == path) ?? false; - if (self) - { - hasPathAccess = true; - return true; - } + return allowed; + } - //is it ancestor? - var ancestor = startNodePaths?.Any(x => x.StartsWith(path)) ?? false; - if (ancestor) - { - //hasPathAccess = false; - return true; - } + 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)); + } - //is it descendant? - var descendant = startNodePaths?.Any(x => path.StartsWith(x)) ?? false; - if (descendant) - { - hasPathAccess = true; - return true; - } + 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 631fe52b28d2..6eb3defc4509 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 { - - /// - public class ExternalLogin : IExternalLogin + /// + /// Initializes a new instance of the class. + /// + public ExternalLogin(string loginProvider, string providerKey, string? userData = null) { - /// - /// 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; - } + 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 85089ddba625..df986d176ff5 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)); - } + 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 LoginProvider { get; } - /// - public string Name { get; } + /// + public string Name { get; } - /// - public string Value { get; } - } + /// + public string Value { get; } } diff --git a/src/Umbraco.Core/Security/IBackofficeSecurity.cs b/src/Umbraco.Core/Security/IBackofficeSecurity.cs index 12b29a02889f..2de9104a95df 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 7ef33ecdc612..11ed86971ee5 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 0c09cecfc0b0..225b0390d38e 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 b3fd4b64b2e8..a5dba5a17e5c 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 9bcfe405ddb3..3faf3cfd4d2a 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 c9eb64ceb3eb..51035b724c5c 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 0e7f22d72f64..f2e17a19afe2 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 c0d436048e2f..5f3345ea73e0 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 6ec9eb7ade61..d830d757f172 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 f0da6c314a85..8d2b12b6f889 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 225d46b26835..83c11916b163 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 +{ /// - /// This class is used by events raised from the BackofficeUserManager + /// Default constructor /// - public class IdentityAuditEventArgs : EventArgs + public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUser, string affectedUsername) { - /// - /// The action that got triggered from the audit event - /// - public AuditEvent Action { get; private set; } - - /// - /// Current date/time in UTC format - /// - public DateTime DateTimeUtc { get; private set; } - - /// - /// The source IP address of the user performing the action - /// - public string IpAddress { get; private set; } - - /// - /// The user affected by the event raised - /// - public string AffectedUser { get; private set; } + DateTimeUtc = DateTime.UtcNow; + Action = action; + IpAddress = ipAddress; + Comment = comment; + PerformingUser = performingUser; + AffectedUsername = affectedUsername; + AffectedUser = affectedUser; + } - /// - /// 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; } + public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUsername) + : this(action, ipAddress, performingUser, comment, Constants.Security.SuperUserIdAsString, affectedUsername) + { + } - /// - /// An optional comment about the action being logged - /// - public string Comment { get; private set; } + /// + /// The action that got triggered from the audit event + /// + public AuditEvent Action { get; } - /// - /// This property is always empty except in the LoginFailed event for an unknown user trying to login - /// - public string AffectedUsername { 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; } - /// - /// 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; - } + /// + /// The user affected by the event raised + /// + public string AffectedUser { get; } - public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUsername) - : this(action, ipAddress, performingUser, comment, Constants.Security.SuperUserIdAsString, affectedUsername) - { - } + /// + /// 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; } - 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 402660ead994..ca821811cc69 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 +{ /// - /// 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(string loginProvider, string providerKey, string userId) { - /// - /// Initializes a new instance of the class. - /// - public IdentityUserLogin(string loginProvider, string providerKey, string userId) - { - LoginProvider = loginProvider; - ProviderKey = providerKey; - UserId = 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; - } + /// + /// 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 LoginProvider { get; set; } - /// - public string ProviderKey { get; set; } + /// + public string ProviderKey { get; set; } - /// - public string UserId { get; set; } + /// + public string UserId { get; set; } - /// - public string? UserData { get; set; } - } + /// + public string? UserData { get; set; } } diff --git a/src/Umbraco.Core/Security/IdentityUserToken.cs b/src/Umbraco.Core/Security/IdentityUserToken.cs index 014001a3a9cf..f4fcd46ace25 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; - } + 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; - } + /// + /// 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 LoginProvider { get; set; } - /// - public string Name { get; set; } + /// + public string Name { get; set; } - /// - public string Value { get; set; } + /// + public string Value { get; set; } - /// - public string? UserId { get; set; } - } + /// + public string? UserId { get; set; } } diff --git a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs index b8c7596b2d0a..3b53509240c0 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); + } - /// - /// Handles password hashing and formatting for legacy hashing algorithms. - /// - /// - /// Should probably be internal. - /// - public class LegacyPasswordSecurity + // 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) { - // 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)); - - string salt; - var hashed = HashNewPassword(algorithmType, password, out salt); - return FormatPasswordForStorage(algorithmType, hashed, salt); + if (string.IsNullOrWhiteSpace(password)) + { + throw new ArgumentException("password cannot be empty", nameof(password)); } - // 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"); - } + var hashed = HashNewPassword(algorithmType, password, out string salt); + return FormatPasswordForStorage(algorithmType, hashed, salt); + } - return salt + hashedPassword; + // 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"); } - /// - /// 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) + return salt + hashedPassword; + } + + /// + /// 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) + { + if (string.IsNullOrWhiteSpace(dbPassword)) { - if (string.IsNullOrWhiteSpace(dbPassword)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword)); - } + throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword)); + } - if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix)) - { - return false; - } + 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; - } + 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; + } + } - /// - /// Verify a legacy hashed password (HMACSHA1) - /// - public bool VerifyLegacyHashedPassword(string password, string dbPassword) + /// + /// Verify a legacy hashed password (HMACSHA1) + /// + public bool VerifyLegacyHashedPassword(string password, string dbPassword) + { + var hashAlgorithm = new HMACSHA1 { - var hashAlgorithm = new HMACSHA1 - { - //the legacy salt was actually the password :( - Key = Encoding.Unicode.GetBytes(password) - }; + // the legacy salt was actually the password :( + Key = Encoding.Unicode.GetBytes(password), + }; - var hashed = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password))); + var hashed = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password))); - return dbPassword == hashed; - } + 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); + /// + /// 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)); } - /// - /// 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 (!SupportHashAlgorithm(algorithm)) { - if (string.IsNullOrWhiteSpace(storedString)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString)); - } + throw new InvalidOperationException($"{algorithm} is not supported"); + } - if (!SupportHashAlgorithm(algorithm)) - { - throw new InvalidOperationException($"{algorithm} is not supported"); - } + var saltLen = GenerateSalt(); + salt = storedString.Substring(0, saltLen.Length); + return storedString.Substring(saltLen.Length); + } - 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; } - public static string GenerateSalt() + // 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")) { - var numArray = new byte[16]; - new RNGCryptoServiceProvider().GetBytes(numArray); - return Convert.ToBase64String(numArray); + return true; } - /// - /// 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"); - } + return false; + } - // This is the correct way to implement this (as per the sql membership provider) + /// + /// 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"); + } - var bytes = Encoding.Unicode.GetBytes(pass); - var saltBytes = Convert.FromBase64String(salt); - byte[] inArray; + // 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) + using HashAlgorithm hashAlgorithm = GetHashAlgorithm(algorithmType); + if (hashAlgorithm is KeyedHashAlgorithm algorithm) + { + KeyedHashAlgorithm keyedHashAlgorithm = algorithm; + if (keyedHashAlgorithm.Key.Length == saltBytes.Length) { - 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); + // 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 { - 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); + // 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; } - return Convert.ToBase64String(inArray); + inArray = keyedHashAlgorithm.ComputeHash(bytes); } - - /// - /// Return the hash algorithm to use based on the - /// - /// The hashing algorithm name. - /// - /// - private HashAlgorithm GetHashAlgorithm(string algorithm) + else { - if (algorithm.IsNullOrWhiteSpace()) - throw new InvalidOperationException("No hash algorithm type specified"); + 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); + } - var alg = HashAlgorithm.Create(algorithm); - if (alg == null) - throw new InvalidOperationException($"The hash algorithm specified {algorithm} cannot be resolved"); + return Convert.ToBase64String(inArray); + } - return alg; + /// + /// 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"); } - public bool SupportHashAlgorithm(string algorithm) + var alg = HashAlgorithm.Create(algorithm); + if (alg == null) { - // 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; + 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 d30ab90af2d1..c46d32f56583 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 { + private readonly AppCaches _appCaches; + + public enum MediaAccess + { + Granted, + Denied, + NotFound, + } + + private readonly IEntityService _entityService; + private readonly IMediaService _mediaService; + + public MediaPermissions(IMediaService mediaService, IEntityService entityService, AppCaches appCaches) + { + _mediaService = mediaService; + _entityService = entityService; + _appCaches = appCaches; + } + /// - /// Checks user access to media + /// 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 /// - public class MediaPermissions + /// + /// The content to lookup, if the contentItem is not specified + /// + /// + public MediaAccess CheckPermissions(IUser? user, int nodeId, out IMedia? media) { - private readonly IMediaService _mediaService; - private readonly IEntityService _entityService; - private readonly AppCaches _appCaches; - - public enum MediaAccess + 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); - } + return MediaAccess.NotFound; + } - 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); - 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 hasPathAccess ? MediaAccess.Granted : MediaAccess.Denied; + public MediaAccess CheckPermissions(IMedia? media, IUser? user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); } - public MediaAccess CheckPermissions(IMedia? media, IUser? user) + if (media == null) { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (media == null) return MediaAccess.NotFound; + return MediaAccess.NotFound; + } - var hasPathAccess = user.HasPathAccess(media, _entityService, _appCaches); + var hasPathAccess = user.HasPathAccess(media, _entityService, _appCaches); - return hasPathAccess ? MediaAccess.Granted : MediaAccess.Denied; - } + return hasPathAccess ? MediaAccess.Granted : MediaAccess.Denied; } } diff --git a/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs b/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs index 2ada23631aa1..5892f786a747 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 55a6ba1a5108..0a3e8925adaa 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 { - /// - /// Generates a password - /// - /// - /// This uses logic copied from the old MembershipProvider.GeneratePassword logic - /// - public class PasswordGenerator - { - private readonly IPasswordConfiguration _passwordConfiguration; + private readonly IPasswordConfiguration _passwordConfiguration; - public PasswordGenerator(IPasswordConfiguration passwordConfiguration) - { - _passwordConfiguration = passwordConfiguration; - } - public string GeneratePassword() - { - var password = PasswordStore.GeneratePassword( - _passwordConfiguration.RequiredLength, - _passwordConfiguration.GetMinNonAlphaNumericChars()); + public PasswordGenerator(IPasswordConfiguration passwordConfiguration) => + _passwordConfiguration = passwordConfiguration; - var random = new Random(); + public string GeneratePassword() + { + var password = PasswordStore.GeneratePassword( + _passwordConfiguration.RequiredLength, + _passwordConfiguration.GetMinNonAlphaNumericChars()); - var passwordChars = password.ToCharArray(); + var random = new Random(); - if (_passwordConfiguration.RequireDigit && passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x))) - password += Convert.ToChar(random.Next(48, 58)); // 0-9 + var passwordChars = password.ToCharArray(); - if (_passwordConfiguration.RequireLowercase && passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x))) - password += Convert.ToChar(random.Next(97, 123)); // a-z + if (_passwordConfiguration.RequireDigit && + passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x))) + { + password += Convert.ToChar(random.Next(48, 58)); // 0-9 + } - if (_passwordConfiguration.RequireUppercase && passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x))) - password += Convert.ToChar(random.Next(65, 91)); // A-Z + if (_passwordConfiguration.RequireLowercase && + passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x))) + { + password += Convert.ToChar(random.Next(97, 123)); // a-z + } - if (_passwordConfiguration.RequireNonLetterOrDigit && passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x))) - password += Convert.ToChar(random.Next(33, 48)); // symbols !"#$%&'()*+,-./ + if (_passwordConfiguration.RequireUppercase && + passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x))) + { + password += Convert.ToChar(random.Next(65, 91)); // A-Z + } - return password; + if (_passwordConfiguration.RequireNonLetterOrDigit && + passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x))) + { + password += Convert.ToChar(random.Next(33, 48)); // symbols !"#$%&'()*+,-./ } - /// - /// 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 + return password; + } + + /// + /// 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 readonly char[] Punctuations = "!@#$%^&*()_-+=[{]};:>|./?".ToCharArray(); + private static readonly char[] StartingChars = { '<', '&' }; + + /// 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) { - 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) + if (length < 1 || length > 128) { - 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 + 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; + 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 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) { - 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; - } + chArray[index] = (char)(48 + num2); } - if (num1 < numberOfNonAlphanumericCharacters) + else if (num2 < 36) { - var random = new Random(); - for (var index1 = 0; index1 < numberOfNonAlphanumericCharacters - num1; ++index1) + 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 { - int index2; - do - { - index2 = random.Next(0, length); - } - while (!char.IsLetterOrDigit(chArray[index2])); - chArray[index2] = Punctuations[random.Next(0, Punctuations.Length)]; + index2 = random.Next(0, length); } + while (!char.IsLetterOrDigit(chArray[index2])); + + chArray[index2] = Punctuations[random.Next(0, Punctuations.Length)]; } - s = new string(chArray); } - while (IsDangerousString(s, out matchIndex)); - return s; + + s = new string(chArray); } + while (IsDangerousString(s, out int matchIndex)); + + return s; + } - private static bool IsDangerousString(string s, out int matchIndex) + private static bool IsDangerousString(string s, out int matchIndex) + { + // bool inComment = false; + matchIndex = 0; + + for (var i = 0; ;) { - //bool inComment = false; - matchIndex = 0; + // Look for the start of one of our patterns + var n = s.IndexOfAny(StartingChars, i); - for (var i = 0; ;) + // If not found, the string is safe + if (n < 0) { + return false; + } - // 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; + } - // If it's the last char, it's safe - if (n == s.Length - 1) return false; + matchIndex = n; - 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; + } - 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; - } + break; + case '&': + // If the & is followed by a #, it's unsafe (e.g. S) + if (s[n + 1] == '#') + { + return true; + } - // Continue searching - i = n + 1; + 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 b92c0ff57a5b..9026b11fd5fc 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 b6b6c241e425..0809f6c5018c 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 UpdateMemberProfileStatus Status { get; private set; } - public static UpdateMemberProfileResult Success() - { - return new UpdateMemberProfileResult { Status = UpdateMemberProfileStatus.Success }; - } + public string? ErrorMessage { get; private set; } - public static UpdateMemberProfileResult Error(string message) - { - return new UpdateMemberProfileResult { Status = UpdateMemberProfileStatus.Error, ErrorMessage = message }; - } - } + 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 df805d30969e..74fb52e6971c 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 5a04553f1bef..3c33f4308749 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 @@ public sealed class SemVersion : IComparable, IComparable 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 @@ public sealed class SemVersion : IComparable, IComparable, ISerializ
 
 #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 @@ private SemVersion(SerializationInfo info, StreamingContext context)
 #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 @@ private SemVersion(SerializationInfo info, StreamingContext context)
         /// 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 @@ public SemVersion(Version version)
         /// 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 @@ public static SemVersion Parse(string version, bool strict = false)
             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 @@ public static SemVersion Parse(string version, bool strict = false)
                 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 @@ public static SemVersion Parse(string version, bool strict = false)
         }
 
         /// 
-        /// 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 @@ public static bool TryParse(string version, out SemVersion? semver, bool strict
         }
 
         /// 
-        /// Tests the specified versions for equality.
+        ///     Tests the specified versions for equality.
         /// 
         /// The first version.
         /// The second version.
@@ -206,26 +218,34 @@ public static bool TryParse(string version, out SemVersion? semver, bool strict
         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 @@ public static int Compare(SemVersion versionA, SemVersion versionB)
         /// 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 @@ static int CompareComponent(string a, string b, bool lower = false)
         }
 
         /// 
-        /// 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 @@ public override int GetHashCode()
         [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 dee2e4c5db92..9a0429a75e48 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 051055b56407..5a31a2cf971a 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 f7560afa9338..046c5fff3dba 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.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)
+    {
+        _auditRepository = auditRepository;
+        _auditEntryRepository = auditEntryRepository;
+        _isAvailable = new Lazy(DetermineIsAvailable);
+    }
+
+    public void Add(AuditType type, int userId, int objectId, string? entityType, string comment, string? parameters = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _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));
+        }
+
+        if (pageSize <= 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
+        }
+
+        if (entityId == Constants.System.Root || entityId <= 0)
+        {
+            totalRecords = 0;
+            return Enumerable.Empty();
+        }
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            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));
+        }
+
+        if (pageSize <= 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
+        }
+
+        if (userId < Constants.Security.SuperUserId)
+        {
+            totalRecords = 0;
+            return Enumerable.Empty();
+        }
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery query = Query().Where(x => x.UserId == userId);
+
+            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)
     {
-        private readonly Lazy _isAvailable;
-        private readonly IAuditRepository _auditRepository;
-        private readonly IAuditEntryRepository _auditEntryRepository;
-
-        public AuditService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IAuditRepository auditRepository, IAuditEntryRepository auditEntryRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
-        {
-            _auditRepository = auditRepository;
-            _auditEntryRepository = auditEntryRepository;
-            _isAvailable = new Lazy(DetermineIsAvailable);
-        }
-
-        public void Add(AuditType type, int userId, int objectId, string? entityType, string comment, string? parameters = null)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _auditRepository.Save(new AuditItem(objectId, type, userId, entityType, comment, parameters));
-                scope.Complete();
-            }
-        }
-
-        public IEnumerable GetLogs(int objectId)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                var result = _auditRepository.Get(Query().Where(x => x.Id == objectId));
-                scope.Complete();
-                return result;
-            }
-        }
-
-        public IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null)
-        {
-            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;
-            }
-        }
-
-        public IEnumerable GetLogs(AuditType type, DateTime? sinceDate = null)
-        {
-            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;
-            }
-        }
-
-        public void CleanLogs(int maximumAgeOfLogsInMinutes)
-        {
-            using (var 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));
-            if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
-
-            if (entityId == Cms.Core.Constants.System.Root || entityId <= 0)
-            {
-                totalRecords = 0;
-                return Enumerable.Empty();
-            }
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var 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));
-            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);
-            }
-        }
-
-        /// 
-        public IAuditEntry Write(int performingUserId, string perfomingDetails, string performingIp, DateTime eventDateUtc, int affectedUserId, string? affectedDetails, string eventType, string eventDetails)
-        {
-            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));
-
-            //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));
-
-            //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));
-
-            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;
-
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _auditEntryRepository.Save(entry);
-                scope.Complete();
-            }
+        if (performingUserId < 0 && performingUserId != 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));
+        }
+
+        // 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)];
+
+        // 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));
+        }
+
+        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())
+        {
+            _auditEntryRepository.Save(entry);
+            scope.Complete();
+        }
+
+        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()
+    {
+        if (_isAvailable.Value == false)
         {
-            if (_isAvailable.Value == false) return Enumerable.Empty();
+            return Enumerable.Empty();
+        }
 
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _auditEntryRepository.GetMany();
-            }
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            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)
+    // 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)
         {
-            if (_isAvailable.Value == false)
-            {
-                records = 0;
-                return Enumerable.Empty();
-            }
+            records = 0;
+            return Enumerable.Empty();
+        }
 
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _auditEntryRepository.GetPage(pageIndex, pageCount, out records);
-            }
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _auditEntryRepository.GetPage(pageIndex, pageCount, out records);
         }
+    }
 
-        /// 
-        /// Determines whether the repository is available.
-        /// 
-        private bool DetermineIsAvailable()
+    /// 
+    ///     Determines whether the repository is available.
+    /// 
+    private bool DetermineIsAvailable()
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _auditEntryRepository.IsAvailable();
-            }
+            return _auditEntryRepository.IsAvailable();
         }
     }
 }
diff --git a/src/Umbraco.Core/Services/BasicAuthService.cs b/src/Umbraco.Core/Services/BasicAuthService.cs
index d81469fac0f2..02f955bad6d3 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 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;
+    {
+        _basicAuthSettings = optionsMonitor.CurrentValue;
 
-            optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings);
-        }
+        optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings);
+    }
 
-        public BasicAuthService(IOptionsMonitor optionsMonitor, IIpAddressUtilities ipAddressUtilities)
-        {
-            _ipAddressUtilities = ipAddressUtilities;
-            _basicAuthSettings = optionsMonitor.CurrentValue;
+    public BasicAuthService(IOptionsMonitor optionsMonitor, IIpAddressUtilities ipAddressUtilities)
+    {
+        _ipAddressUtilities = ipAddressUtilities;
+        _basicAuthSettings = optionsMonitor.CurrentValue;
 
-            optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings);
-        }
+        optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings);
+    }
 
-        public bool IsBasicAuthEnabled() => _basicAuthSettings.Enabled;
-        public bool IsRedirectToLoginPageEnabled() => _basicAuthSettings.RedirectToLoginPage;
+    public bool IsBasicAuthEnabled() => _basicAuthSettings.Enabled;
+    public bool IsRedirectToLoginPageEnabled() => _basicAuthSettings.RedirectToLoginPage;
 
-        public bool IsIpAllowListed(IPAddress clientIpAddress)
+    public bool IsIpAllowListed(IPAddress clientIpAddress)
+    {
+        foreach (var allowedIpString in _basicAuthSettings.AllowedIPs)
         {
-            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;
+        return false;
+    }
 
-            if (string.IsNullOrWhiteSpace(headerName) || string.IsNullOrWhiteSpace(sharedSecret))
-            {
-                return false;
-            }
+    public bool HasCorrectSharedSecret(IDictionary headers)
+    {
+        var headerName = _basicAuthSettings.SharedSecret.HeaderName;
+        var sharedSecret = _basicAuthSettings.SharedSecret.Value;
 
-            return headers.TryGetValue(headerName, out var value) && value.Equals(sharedSecret);
+        if (string.IsNullOrWhiteSpace(headerName) || string.IsNullOrWhiteSpace(sharedSecret))
+        {
+            return false;
         }
+
+        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 f82340681892..f3fe533373aa 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 9489e52d42bc..d45a2267bc89 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
-{
-    public static class ContentTypeChangeExtensions
-    {
+namespace Umbraco.Extensions;
 
-        public static bool HasType(this ContentTypeChangeTypes change, ContentTypeChangeTypes type)
-        {
-            return (change & type) != ContentTypeChangeTypes.None;
-        }
+public static class ContentTypeChangeExtensions
+{
+    public static bool HasType(this ContentTypeChangeTypes change, ContentTypeChangeTypes type) =>
+        (change & type) != ContentTypeChangeTypes.None;
 
-        public static bool HasTypesAll(this ContentTypeChangeTypes change, ContentTypeChangeTypes types)
-        {
-            return (change & types) == types;
-        }
+    public static bool HasTypesAll(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) =>
+        (change & types) == types;
 
-        public static bool HasTypesAny(this ContentTypeChangeTypes change, ContentTypeChangeTypes types)
-        {
-            return (change & types) != ContentTypeChangeTypes.None;
-        }
+    public static bool HasTypesAny(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) =>
+        (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 cd4965dc2b2e..4346a278cc28 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 25bf48e55aaf..303461f48f89 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 f306a796cccb..bb722dce24e0 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;
-        }
+        Item = changedItem;
+        ChangeTypes = changeTypes;
+    }
 
-        public TItem Item { get; }
-        public TreeChangeTypes ChangeTypes { get; }
+    public TItem Item { get; }
 
-        public EventArgs ToEventArgs()
-        {
-            return new EventArgs(this);
-        }
+    public TreeChangeTypes ChangeTypes { get; }
 
-        public class EventArgs : System.EventArgs
-        {
-            public EventArgs(IEnumerable> changes)
-            {
-                Changes = changes.ToArray();
-            }
+    public EventArgs ToEventArgs() => new EventArgs(this);
 
-            public EventArgs(TreeChange change)
-                : this(new[] { change })
-            { }
+    public class EventArgs : System.EventArgs
+    {
+        public EventArgs(IEnumerable> changes) => Changes = changes.ToArray();
 
-            public IEnumerable> Changes { get; private set; }
+        public EventArgs(TreeChange change)
+            : this(new[] { change })
+        {
         }
+
+        public IEnumerable> Changes { get; }
     }
 }
diff --git a/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs b/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs
index 5de6ae984726..1dc972eb7a35 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 9ef231ac066c..85db740a56ee 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 d37e2e4d0f5f..d7bb7af13ebc 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;
+        // prevent stupid states
+        var v = 0;
+        if ((state & ConsentState.Pending) > 0)
+        {
+            v++;
+        }
+
+        if ((state & ConsentState.Granted) > 0)
+        {
+            v++;
+        }
+
+        if ((state & ConsentState.Revoked) > 0)
+        {
+            v++;
+        }
 
-        /// 
-        /// Initializes a new instance of the  class.
-        /// 
-        public ConsentService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IConsentRepository consentRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+        if (v != 1)
         {
-            _consentRepository = consentRepository;
+            throw new ArgumentException("Invalid state.", nameof(state));
         }
 
-        /// 
-        public IConsent RegisterConsent(string source, string context, string action, ConsentState state, string? comment = null)
+        var consent = new Consent
         {
-            // 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 (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)
             {
-                Current = true,
-                Source = source,
-                Context = context,
-                Action = action,
-                CreateDate = DateTime.Now,
-                State = state,
-                Comment = comment
-            };
-
-            using (var scope = ScopeProvider.CreateCoreScope())
+                query = sourceStartsWith
+                    ? query.Where(x => x.Source!.StartsWith(source))
+                    : query.Where(x => x.Source == source);
+            }
+
+            if (string.IsNullOrWhiteSpace(context) == false)
             {
-                _consentRepository.ClearCurrent(source, context, action);
-                _consentRepository.Save(consent);
-                scope.Complete();
+                query = contextStartsWith
+                    ? query.Where(x => x.Context!.StartsWith(context))
+                    : query.Where(x => x.Context == context);
             }
 
-            return consent;
-        }
+            if (string.IsNullOrWhiteSpace(action) == false)
+            {
+                query = actionStartsWith
+                    ? query.Where(x => x.Action!.StartsWith(action))
+                    : query.Where(x => x.Action == action);
+            }
 
-        /// 
-        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))
+            if (includeHistory == 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 = 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 a2fa13a34689..1cc0081cbb46 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.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)
-        {
-            _documentRepository = documentRepository;
-            _entityRepository = entityRepository;
-            _auditRepository = auditRepository;
-            _contentTypeRepository = contentTypeRepository;
-            _documentBlueprintRepository = documentBlueprintRepository;
-            _languageRepository = languageRepository;
-            _propertyValidationService = propertyValidationService;
-            _shortStringHelper = shortStringHelper;
-            _logger = loggerFactory.CreateLogger();
-        }
+    #region Static queries
 
-        #endregion
+    // lazy-constructed because when the ctor runs, the query factory may not be ready
+    private IQuery QueryNotTrashed =>
+        _queryNotTrashed ??= Query().Where(x => x.Trashed == false);
 
-        #region Static queries
+    #endregion
 
-        // lazy-constructed because when the ctor runs, the query factory may not be ready
+    #region Rollback
 
-        private IQuery QueryNotTrashed =>
-            _queryNotTrashed ??= Query().Where(x => x.Trashed == false);
+    public OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
 
-        #endregion
+        // Get the current copy of the node
+        IContent? content = GetById(id);
 
-        #region Rollback
+        // Get the version
+        IContent? version = GetVersion(versionId);
 
-        public OperationResult Rollback(int id, int versionId, string culture = "*",
-            int userId = Constants.Security.SuperUserId)
+        // Good old null checks
+        if (content == null || version == null || content.Trashed)
         {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            // Get the current copy of the node
-            IContent? content = GetById(id);
+            return new OperationResult(OperationResultType.FailedCannot, evtMsgs);
+        }
 
-            // Get the version
-            IContent? version = GetVersion(versionId);
+        // Store the result of doing the save of content for the rollback
+        OperationResult rollbackSaveResult;
 
-            // Good old null checks
-            if (content == null || version == null || content.Trashed)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var rollingBackNotification = new ContentRollingBackNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(rollingBackNotification))
             {
-                return new OperationResult(OperationResultType.FailedCannot, evtMsgs);
+                scope.Complete();
+                return OperationResult.Cancel(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);
+            // 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));
+            // Save the content for the rollback
+            rollbackSaveResult = Save(content, userId);
 
-                    // 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}'");
-                }
+            // 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));
 
-                scope.Complete();
+                // 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}'");
             }
 
-            return rollbackSaveResult;
+            scope.Complete();
         }
 
-        #endregion
+        return rollbackSaveResult;
+    }
+
+    #endregion
 
-        #region Count
+    #region Count
 
-        public int CountPublished(string? contentTypeAlias = null)
+    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);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.CountPublished(contentTypeAlias);
         }
+    }
 
-        public int Count(string? contentTypeAlias = null)
+    public int Count(string? contentTypeAlias = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.Count(contentTypeAlias);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.Count(contentTypeAlias);
         }
+    }
 
-        public int CountChildren(int parentId, string? contentTypeAlias = null)
+    public int CountChildren(int parentId, string? contentTypeAlias = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.CountChildren(parentId, contentTypeAlias);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.CountChildren(parentId, contentTypeAlias);
         }
+    }
 
-        public int CountDescendants(int parentId, string? contentTypeAlias = null)
+    public int CountDescendants(int parentId, string? contentTypeAlias = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.CountDescendants(parentId, contentTypeAlias);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.CountDescendants(parentId, contentTypeAlias);
         }
+    }
 
-        #endregion
+    #endregion
 
-        #region Permissions
+    #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)
+    /// 
+    ///     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())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-                _documentRepository.ReplaceContentPermissions(permissionSet);
-                scope.Complete();
-            }
+            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)
+    /// 
+    ///     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())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-                _documentRepository.AssignEntityPermission(entity, permission, groupIds);
-                scope.Complete();
-            }
+            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)
+    /// 
+    ///     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))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetPermissionsForEntity(content.Id);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetPermissionsForEntity(content.Id);
         }
+    }
 
-        #endregion
+    #endregion
 
-        #region Create
+    #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?
+    /// 
+    ///     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);
+    }
 
-            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 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)
+    /// 
+    ///     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)
         {
-            // TODO: what about culture?
-
-            IContentType contentType = GetContentType(contentTypeAlias);
-            return Create(name, parentId, contentType, userId);
+            throw new ArgumentException("Content type must be specified", nameof(contentType));
         }
 
-        /// 
-        ///     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)
+        IContent? parent = parentId > 0 ? GetById(parentId) : null;
+        if (parentId > 0 && parent is null)
         {
-            if (contentType is null)
-            {
-                throw new ArgumentException("Content type must be specified", nameof(contentType));
-            }
+            throw new ArgumentException("No content with that id.", nameof(parentId));
+        }
 
-            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);
 
-            var content = new Content(name, parentId, contentType, userId);
+        return content;
+    }
 
-            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
         }
 
-        /// 
-        ///     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)
+        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))
         {
-            // TODO: what about culture?
+            // locking the content tree secures content types too
+            scope.WriteLock(Constants.Locks.ContentTree);
 
-            if (parent == null)
+            IContentType contentType = GetContentType(contentTypeAlias); // + locks
+            if (contentType == null)
             {
-                throw new ArgumentNullException(nameof(parent));
+                throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
             }
 
-            IContentType contentType = GetContentType(contentTypeAlias);
-            if (contentType == null)
+            IContent? parent = parentId > 0 ? GetById(parentId) : null; // + locks
+            if (parentId > 0 && parent == null)
             {
-                throw new ArgumentException("No content type with that alias.",
-                    nameof(contentTypeAlias)); // causes rollback
+                throw new ArgumentException("No content with that id.", nameof(parentId)); // causes rollback
             }
 
-            var content = new Content(name, parent, contentType, userId);
+            Content content = parentId > 0
+                ? new Content(name, parent!, contentType, userId)
+                : new Content(name, parentId, 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)
+    /// 
+    ///     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?
-
-            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;
-            }
+            throw new ArgumentNullException(nameof(parent));
         }
 
-        /// 
-        ///     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?
+            // locking the content tree secures content types too
+            scope.WriteLock(Constants.Locks.ContentTree);
 
-            if (parent == null)
+            IContentType contentType = GetContentType(contentTypeAlias); // + locks
+            if (contentType == null)
             {
-                throw new ArgumentNullException(nameof(parent));
+                throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
             }
 
-            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
-                }
-
-                var content = new Content(name, parent, contentType, userId);
+            var content = new Content(name, parent, contentType, userId);
 
-                Save(content, userId);
+            Save(content, userId);
 
-                return content;
-            }
+            return content;
         }
+    }
 
-        #endregion
+    #endregion
 
-        #region Get, Has, Is
+    #region Get, Has, Is
 
-        /// 
-        ///     Gets an  object by Id
-        /// 
-        /// Id of the Content to retrieve
-        /// 
-        ///     
-        /// 
-        public IContent? GetById(int id)
+    /// 
+    ///     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);
+            return _documentRepository.Get(id);
         }
+    }
 
-        /// 
-        ///     Gets an  object by Id
-        /// 
-        /// Ids of the Content to retrieve
-        /// 
-        ///     
-        /// 
-        public IEnumerable GetByIds(IEnumerable ids)
+    /// 
+    ///     Gets an  object by Id
+    /// 
+    /// Ids of the Content to retrieve
+    /// 
+    ///     
+    /// 
+    public IEnumerable GetByIds(IEnumerable ids)
+    {
+        var idsA = ids.ToArray();
+        if (idsA.Length == 0)
         {
-            var idsA = ids.ToArray();
-            if (idsA.Length == 0)
-            {
-                return Enumerable.Empty();
-            }
-
-            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();
         }
 
-        /// 
-        ///     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))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.Get(key);
-            }
+            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();
         }
+    }
 
-        /// 
-        public ContentScheduleCollection GetContentScheduleByContentId(int contentId)
+    /// 
+    ///     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))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.ContentTree);
-                return _documentRepository.GetContentSchedule(contentId);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.Get(key);
         }
+    }
 
-        /// 
-        public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule)
+    /// 
+    public ContentScheduleCollection GetContentScheduleByContentId(int contentId)
+    {
+        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.GetContentSchedule(contentId);
         }
+    }
 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        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)
+    /// 
+    public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            Guid[] idsA = ids.ToArray();
-            if (idsA.Length == 0)
-            {
-                return Enumerable.Empty();
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IEnumerable? items = _documentRepository.GetMany(idsA);
-
-                if (items is not null)
-                {
-                    var index = items.ToDictionary(x => x.Key, x => x);
+            scope.WriteLock(Constants.Locks.ContentTree);
+            _documentRepository.PersistContentSchedule(content, contentSchedule);
+        }
+    }
 
-                    return idsA.Select(x => index.TryGetValue(x, out IContent? c) ? c : null).WhereNotNull();
-                }
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    Attempt IContentServiceBase.Save(IEnumerable contents, int userId) =>
+        Attempt.Succeed(Save(contents, userId));
 
-                return Enumerable.Empty();
-            }
+    /// 
+    ///     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();
         }
 
-        /// 
-        public IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize,
-            out long totalRecords
-            , IQuery? filter = null, Ordering? ordering = null)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            if (pageIndex < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IEnumerable? items = _documentRepository.GetMany(idsA);
 
-            if (pageSize <= 0)
+            if (items is not null)
             {
-                throw new ArgumentOutOfRangeException(nameof(pageSize));
-            }
+                var index = items.ToDictionary(x => x.Key, x => x);
 
-            if (ordering == null)
-            {
-                ordering = Ordering.By("sortOrder");
+                return idsA.Select(x => index.TryGetValue(x, out IContent? c) ? c : null).WhereNotNull();
             }
 
-            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);
-            }
+            return Enumerable.Empty();
         }
+    }
 
-        /// 
-        public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize,
-            out long totalRecords, IQuery? filter, Ordering? ordering = null)
+    /// 
+    public IEnumerable GetPagedOfType(
+        int contentTypeId,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null)
+    {
+        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);
-            }
+            ordering = Ordering.By("sortOrder");
         }
 
-        /// 
-        ///     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))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetAllVersions(id);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetPage(
+                Query()?.Where(x => x.ContentTypeId == contentTypeId),
+                pageIndex,
+                pageSize,
+                out totalRecords,
+                filter,
+                ordering);
         }
+    }
 
-        /// 
-        ///     Gets a collection of an  objects versions by Id
-        /// 
-        /// An Enumerable list of  objects
-        public IEnumerable GetVersionsSlim(int id, int skip, int take)
+    /// 
+    public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery? filter, Ordering? ordering = null)
+    {
+        if (pageIndex < 0)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetAllVersionsSlim(id, skip, take);
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
         }
 
-        /// 
-        ///     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)
+        if (pageSize <= 0)
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _documentRepository.GetVersionIds(id, maxRows);
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
         }
 
-        /// 
-        ///     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)
+        if (ordering == null)
         {
-            // intentionally not locking
-            IContent? content = GetById(id);
-            if (content is null)
-            {
-                return Enumerable.Empty();
-            }
-
-            return GetAncestors(content);
+            ordering = Ordering.By("sortOrder");
         }
 
-        /// 
-        ///     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)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            //null check otherwise we get exceptions
-            if (content.Path.IsNullOrWhiteSpace())
-            {
-                return Enumerable.Empty();
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetPage(
+                Query()?.Where(x => contentTypeIds.Contains(x.ContentTypeId)),
+                pageIndex,
+                pageSize,
+                out totalRecords,
+                filter,
+                ordering);
+        }
+    }
 
-            var ids = content.GetAncestorIds()?.ToArray();
-            if (ids?.Any() == false)
-            {
-                return new List();
-            }
+    /// 
+    ///     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);
+        }
+    }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetMany(ids!);
-            }
+    /// 
+    ///     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 published  objects by Parent Id
-        /// 
-        /// Id of the Parent to retrieve Children from
-        /// An Enumerable list of published  objects
-        public IEnumerable GetPublishedChildren(int id)
+    /// 
+    ///     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))
         {
-            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);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetAllVersions(id);
         }
+    }
 
-        /// 
-        public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren,
-            IQuery? filter = null, Ordering? ordering = null)
+    /// 
+    ///     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))
         {
-            if (pageIndex < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetAllVersionsSlim(id, skip, take);
+        }
+    }
 
-            if (pageSize <= 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageSize));
-            }
+    /// 
+    ///     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);
+        }
+    }
 
-            if (ordering == null)
-            {
-                ordering = Ordering.By("sortOrder");
-            }
+    /// 
+    ///     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();
+        }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
+        return GetAncestors(content);
+    }
 
-                IQuery? query = Query()?.Where(x => x.ParentId == id);
-                return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
-            }
+    /// 
+    ///     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();
         }
 
-        /// 
-        public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren,
-            IQuery? filter = null, Ordering? ordering = null)
+        var ids = content.GetAncestorIds()?.ToArray();
+        if (ids?.Any() == false)
         {
-            if (ordering == null)
-            {
-                ordering = Ordering.By("Path");
-            }
+            return new List();
+        }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetMany(ids!);
+        }
+    }
 
-                //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();
-                    }
+    /// 
+    ///     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);
+        }
+    }
 
-                    return GetPagedLocked(GetPagedDescendantQuery(contentPath[0].Path), pageIndex, pageSize,
-                        out totalChildren, filter, ordering);
-                }
+    /// 
+    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));
+        }
 
-                return GetPagedLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
-            }
+        if (pageSize <= 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
         }
 
-        private IQuery? GetPagedDescendantQuery(string contentPath)
+        if (ordering == null)
         {
-            IQuery? query = Query();
-            if (!contentPath.IsNullOrWhiteSpace())
-            {
-                query?.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
-            }
+            ordering = Ordering.By("sortOrder");
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
 
-            return query;
+            IQuery? query = Query()?.Where(x => x.ParentId == id);
+            return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
         }
+    }
 
-        private IEnumerable GetPagedLocked(IQuery? query, long pageIndex, int pageSize,
-            out long totalChildren,
-            IQuery? filter, Ordering? ordering)
+    /// 
+    public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, IQuery? filter = null, Ordering? ordering = null)
+    {
+        if (ordering == null)
         {
-            if (pageIndex < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            }
+            ordering = Ordering.By("Path");
+        }
 
-            if (pageSize <= 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageSize));
-            }
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
 
-            if (ordering == null)
+            // if the id is System Root, then just get all
+            if (id != Constants.System.Root)
             {
-                throw new ArgumentNullException(nameof(ordering));
+                TreeEntityPath[] contentPath =
+                    _entityRepository.GetAllPaths(Constants.ObjectTypes.Document, id).ToArray();
+                if (contentPath.Length == 0)
+                {
+                    totalChildren = 0;
+                    return Enumerable.Empty();
+                }
+
+                return GetPagedLocked(GetPagedDescendantQuery(contentPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering);
             }
 
-            return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
+            return GetPagedLocked(null, 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)
+    private IQuery? GetPagedDescendantQuery(string contentPath)
+    {
+        IQuery? query = Query();
+        if (!contentPath.IsNullOrWhiteSpace())
         {
-            // intentionally not locking
-            IContent? content = GetById(id);
-            return GetParent(content);
+            query?.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
         }
 
-        /// 
-        ///     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)
-            {
-                return null;
-            }
+        return query;
+    }
 
-            return GetById(content.ParentId);
+    private IEnumerable GetPagedLocked(IQuery? query, long pageIndex, int pageSize, out long totalChildren, IQuery? filter, Ordering? ordering)
+    {
+        if (pageIndex < 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
         }
 
-        /// 
-        ///     Gets a collection of  objects, which reside at the first level / root
-        /// 
-        /// An Enumerable list of  objects
-        public IEnumerable GetRootContent()
+        if (pageSize <= 0)
         {
-            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);
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
         }
 
-        /// 
-        ///     Gets all published content items
-        /// 
-        /// 
-        internal IEnumerable GetAllPublished()
+        if (ordering == null)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.Get(QueryNotTrashed);
-            }
+            throw new ArgumentNullException(nameof(ordering));
         }
 
-        /// 
-        public IEnumerable GetContentForExpiration(DateTime date)
+        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.GetContentForExpiration(date);
-            }
+            return null;
         }
 
-        /// 
-        public IEnumerable GetContentForRelease(DateTime date)
+        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.GetContentForRelease(date);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IQuery query = Query().Where(x => x.ParentId == Constants.System.Root);
+            return _documentRepository.Get(query);
         }
+    }
 
-        /// 
-        ///     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)
+    /// 
+    ///     Gets all published content items
+    /// 
+    /// 
+    internal IEnumerable GetAllPublished()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                if (ordering == null)
-                {
-                    ordering = Ordering.By("Path");
-                }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.Get(QueryNotTrashed);
+        }
+    }
 
-                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);
-            }
+    /// 
+    public IEnumerable GetContentForExpiration(DateTime date)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetContentForExpiration(date);
         }
+    }
 
-        /// 
-        ///     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;
+    /// 
+    public IEnumerable GetContentForRelease(DateTime date)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetContentForRelease(date);
+        }
+    }
 
-        /// 
-        ///     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)
+    /// 
+    ///     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))
         {
-            // fast
-            if (content.ParentId == Constants.System.Root)
+            if (ordering == null)
             {
-                return true; // root content is always publishable
+                ordering = Ordering.By("Path");
             }
 
-            if (content.Trashed)
-            {
-                return false; // trashed content is never publishable
-            }
+            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);
+        }
+    }
 
-            // not trashed and has a parent: publishable if the parent is path-published
-            IContent? parent = GetById(content.ParentId);
-            return parent == null || IsPathPublished(parent);
+    /// 
+    ///     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
         }
 
-        public bool IsPathPublished(IContent? content)
+        if (content.Trashed)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.IsPathPublished(content);
-            }
+            return false; // trashed content is never publishable
         }
 
-        #endregion
+        // 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);
+        }
+    }
 
-        #region Save, Publish, Unpublish
+    #endregion
 
-        /// 
-        public OperationResult Save(IContent content, int? userId = null,
-            ContentScheduleCollection? contentSchedule = null)
+    #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)
         {
-            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.");
-            }
+            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.");
-            }
+        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();
+        EventMessages eventMessages = EventMessagesFactory.Get();
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var savingNotification = new ContentSavingNotification(content, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                var savingNotification = new ContentSavingNotification(content, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Cancel(eventMessages);
-                }
+                scope.Complete();
+                return OperationResult.Cancel(eventMessages);
+            }
 
-                scope.WriteLock(Constants.Locks.ContentTree);
-                userId ??= Constants.Security.SuperUserId;
+            scope.WriteLock(Constants.Locks.ContentTree);
+            userId ??= Constants.Security.SuperUserId;
 
-                if (content.HasIdentity == false)
-                {
-                    content.CreatorId = userId.Value;
-                }
+            if (content.HasIdentity == false)
+            {
+                content.CreatorId = userId.Value;
+            }
 
-                content.WriterId = 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.
+            // track the cultures that have changed
+            List? culturesChanging = content.ContentType.VariesByCulture()
+                ? content.CultureInfos?.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList()
+                : null;
 
-                _documentRepository.Save(content);
+            // 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);
-                }
+            if (contentSchedule != null)
+            {
+                _documentRepository.PersistContentSchedule(content, contentSchedule);
+            }
 
-                scope.Notifications.Publish(
-                    new ContentSavedNotification(content, eventMessages).WithStateFrom(savingNotification));
+            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));
+            // 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
+            if (culturesChanging != null)
+            {
+                IEnumerable? languages = _languageRepository.GetMany()?
+                    .Where(x => culturesChanging.InvariantContains(x.IsoCode))
+                    .Select(x => x.CultureName);
+                if (languages is not null)
                 {
-                    Audit(AuditType.Save, userId.Value, content.Id);
+                    var langs = string.Join(", ", languages);
+                    Audit(AuditType.SaveVariant, userId.Value, content.Id, $"Saved languages: {langs}", langs);
                 }
-
-                scope.Complete();
+            }
+            else
+            {
+                Audit(AuditType.Save, userId.Value, content.Id);
             }
 
-            return OperationResult.Succeed(eventMessages);
+            scope.Complete();
         }
 
-        /// 
-        public OperationResult Save(IEnumerable contents, int userId = Constants.Security.SuperUserId)
+        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())
         {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            IContent[] contentsA = contents.ToArray();
+            var savingNotification = new ContentSavingNotification(contentsA, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return OperationResult.Cancel(eventMessages);
+            }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            scope.WriteLock(Constants.Locks.ContentTree);
+            foreach (IContent content in contentsA)
             {
-                var savingNotification = new ContentSavingNotification(contentsA, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
+                if (content.HasIdentity == false)
                 {
-                    scope.Complete();
-                    return OperationResult.Cancel(eventMessages);
+                    content.CreatorId = userId;
                 }
 
-                scope.WriteLock(Constants.Locks.ContentTree);
-                foreach (IContent content in contentsA)
-                {
-                    if (content.HasIdentity == false)
-                    {
-                        content.CreatorId = userId;
-                    }
-
-                    content.WriterId = userId;
+                content.WriterId = userId;
 
-                    _documentRepository.Save(content);
-                }
+                _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));
+            scope.Notifications.Publish(
+                new ContentSavedNotification(contentsA, eventMessages).WithStateFrom(savingNotification));
 
-                Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Saved multiple content");
+            // TODO: See note above about supressing events
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(contentsA, TreeChangeTypes.RefreshNode, eventMessages));
 
-                scope.Complete();
-            }
+            Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Saved multiple content");
 
-            return OperationResult.Succeed(eventMessages);
+            scope.Complete();
         }
 
-        /// 
-        public PublishResult SaveAndPublish(IContent content, string culture = "*",
-            int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
+        return OperationResult.Succeed(eventMessages);
+    }
 
-            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.");
-            }
+    /// 
+    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())
+        // 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())
             {
-                if (culture.IsNullOrWhiteSpace())
-                {
-                    throw new NotSupportedException("Invariant culture is not supported by variant content types.");
-                }
+                throw new NotSupportedException("Invariant culture is not supported by variant content types.");
             }
-            else
+        }
+        else
+        {
+            if (!culture.IsNullOrWhiteSpace() && culture != "*")
             {
-                if (!culture.IsNullOrWhiteSpace() && culture != "*")
-                {
-                    throw new NotSupportedException(
-                        $"Culture \"{culture}\" is not supported by invariant content types.");
-                }
+                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.");
+        }
 
-            if (content.Name != null && content.Name.Length > 255)
+        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))
             {
-                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
             }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
+            // if culture is specific, first publish the invariant values, then publish the culture itself.
+            // if culture is '*', then publish them all (including variants)
 
-                var allLangs = _languageRepository.GetMany().ToList();
+            // this will create the correct culture impact even if culture is * or null
+            var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content);
 
-                var savingNotification = new ContentSavingNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, 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);
 
-                // if culture is specific, first publish the invariant values, then publish the culture itself.
-                // if culture is '*', then publish them all (including variants)
+            PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+            scope.Complete();
+            return result;
+        }
+    }
 
-                //this will create the correct culture impact even if culture is * or null
-                var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content);
+    /// 
+    public PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId)
+    {
+        if (content == null)
+        {
+            throw new ArgumentNullException(nameof(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);
+        if (cultures == null)
+        {
+            throw new ArgumentNullException(nameof(cultures));
+        }
 
-                PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs,
-                    savingNotification.State, userId);
-                scope.Complete();
-                return result;
-            }
+        if (content.Name != null && content.Name.Length > 255)
+        {
+            throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
         }
 
-        /// 
-        public PublishResult SaveAndPublish(IContent content, string[] cultures,
-            int userId = Constants.Security.SuperUserId)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            if (content == null)
-            {
-                throw new ArgumentNullException(nameof(content));
-            }
+            scope.WriteLock(Constants.Locks.ContentTree);
 
-            if (cultures == null)
+            var allLangs = _languageRepository.GetMany().ToList();
+
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+
+            var savingNotification = new ContentSavingNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                throw new ArgumentNullException(nameof(cultures));
+                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
             }
 
-            if (content.Name != null && content.Name.Length > 255)
+            var varies = content.ContentType.VariesByCulture();
+
+            if (cultures.Length == 0 && !varies)
             {
-                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+                // No cultures specified and doesn't vary, so publish it, else nothing to publish
+                return SaveAndPublish(content, userId: userId);
             }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            if (cultures.Any(x => x == null || x == "*"))
             {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var allLangs = _languageRepository.GetMany().ToList();
-
-                EventMessages evtMsgs = EventMessagesFactory.Get();
+                throw new InvalidOperationException(
+                    "Only valid cultures are allowed to be used in this method, wildcards or nulls are not allowed");
+            }
 
-                var savingNotification = new ContentSavingNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
-                }
+            IEnumerable impacts =
+                cultures.Select(x => CultureImpact.Explicit(x, IsDefaultCulture(allLangs, x)));
 
-                var varies = content.ContentType.VariesByCulture();
+            // 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);
+            }
 
-                if (cultures.Length == 0 && !varies)
-                {
-                    //no cultures specified and doesn't vary, so publish it, else nothing to publish
-                    return SaveAndPublish(content, userId: userId);
-                }
+            PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+            scope.Complete();
+            return result;
+        }
+    }
 
-                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");
-                }
+    /// 
+    public PublishResult Unpublish(IContent content, string? culture = "*", int userId = Constants.Security.SuperUserId)
+    {
+        if (content == null)
+        {
+            throw new ArgumentNullException(nameof(content));
+        }
 
-                IEnumerable impacts =
-                    cultures.Select(x => CultureImpact.Explicit(x, IsDefaultCulture(allLangs, x)));
+        EventMessages evtMsgs = EventMessagesFactory.Get();
 
-                // 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);
-                }
+        culture = culture?.NullOrWhiteSpaceAsNull();
 
-                PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs,
-                    savingNotification.State, userId);
-                scope.Complete();
-                return result;
-            }
+        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.");
         }
 
-        /// 
-        public PublishResult Unpublish(IContent content, string? culture = "*",
-            int userId = Constants.Security.SuperUserId)
+        // 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 (content == null)
+            if (culture == null)
             {
-                throw new ArgumentNullException(nameof(content));
+                throw new NotSupportedException("Invariant culture is not supported by variant content types.");
             }
-
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            culture = culture?.NullOrWhiteSpaceAsNull();
-
-            PublishedState publishedState = content.PublishedState;
-            if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
+        }
+        else
+        {
+            if (culture != null && culture != "*")
             {
-                throw new InvalidOperationException(
-                    $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method.");
+                throw new NotSupportedException(
+                    $"Culture \"{culture}\" is not supported by invariant content types.");
             }
+        }
 
-            // 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 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))
             {
-                if (culture != null && culture != "*")
-                {
-                    throw new NotSupportedException(
-                        $"Culture \"{culture}\" is not supported by invariant content types.");
-                }
+                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
             }
 
-            // if the content is not published, nothing to do
-            if (!content.Published)
+            // all cultures = unpublish whole
+            if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null))
             {
-                return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
+                // 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;
             }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            else
             {
-                scope.WriteLock(Constants.Locks.ContentTree);
+                // 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);
 
-                var allLangs = _languageRepository.GetMany().ToList();
+                // Save and publish any changes
+                PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
 
-                var savingNotification = new ContentSavingNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
-                }
+                scope.Complete();
 
-                // all cultures = unpublish whole
-                if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null))
+                // 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)
                 {
-                    // 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;
+                    return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
                 }
-                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;
-                }
+                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)
+    /// 
+    ///     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())
         {
-            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))
             {
-                EventMessages evtMsgs = EventMessagesFactory.Get();
+                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
+            }
 
-                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);
-                }
+            PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+            scope.Complete();
+            return result;
+        }
+    }
 
-                var allLangs = _languageRepository.GetMany().ToList();
+    /// 
+    ///     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));
+        }
 
-                PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs,
-                    savingNotification.State, userId);
-                scope.Complete();
-                return result;
-            }
+        if (content == null)
+        {
+            throw new ArgumentNullException(nameof(content));
         }
 
-        /// 
-        ///     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 (eventMessages == null)
         {
-            if (scope == null)
-            {
-                throw new ArgumentNullException(nameof(scope));
-            }
+            throw new ArgumentNullException(nameof(eventMessages));
+        }
 
-            if (content == null)
-            {
-                throw new ArgumentNullException(nameof(content));
-            }
+        PublishResult? publishResult = null;
+        PublishResult? unpublishResult = null;
 
-            if (eventMessages == null)
-            {
-                throw new ArgumentNullException(nameof(eventMessages));
-            }
+        // 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;
 
-            PublishResult? publishResult = null;
-            PublishResult? unpublishResult = null;
+        var isNew = !content.HasIdentity;
+        TreeChangeTypes changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch;
+        var previouslyPublished = content.HasIdentity && content.Published;
 
-            // nothing set = republish it all
-            if (content.PublishedState != PublishedState.Publishing &&
-                content.PublishedState != PublishedState.Unpublishing)
+        // 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)
             {
-                content.PublishedState = PublishedState.Publishing;
+                c.CreatorId = userId;
             }
 
-            // 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;
+            c.WriterId = userId;
 
-            var variesByCulture = content.ContentType.VariesByCulture();
+            // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing
+            _documentRepository.Save(c);
+        }
 
-            //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()
+        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;
 
-            var isNew = !content.HasIdentity;
-            TreeChangeTypes changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch;
-            var previouslyPublished = content.HasIdentity && content.Published;
+            // 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);
 
-            //inline method to persist the document with the documentRepository since this logic could be called a couple times below
-            void SaveDocument(IContent c)
+                    // Set the flag to unpublish and continue
+                    unpublishing = content.Published; // if not published yet, nothing to do
+                }
+            }
+            else
             {
-                // save, always
-                if (c.HasIdentity == false)
+                // in a branch, just give up
+                if (branchOne && !branchRoot)
                 {
-                    c.CreatorId = userId;
+                    return publishResult;
                 }
 
-                c.WriterId = userId;
+                // 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
+                }
 
-                // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing
-                _documentRepository.Save(c);
+                // 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;
             }
+        }
 
-            if (publishing)
+        // 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)
             {
-                //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;
+                return new PublishResult(PublishResultType.FailedPublishConcurrencyViolation, eventMessages, content);
+            }
 
-                // 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)
+            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)
                 {
-                    // 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
-                    }
+                    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,2167 +1521,2126 @@ void SaveDocument(IContent c)
                     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.");
+            }
+        }
 
-            if (unpublishing) // won't happen in a branch
+        // Persist the document
+        SaveDocument(content);
+
+        // raise the Saved event, always
+        scope.Notifications.Publish(
+            new ContentSavedNotification(content, eventMessages).WithState(notificationState));
+
+        // we have tried to unpublish - won't happen in a branch
+        if (unpublishing)
+        {
+            // and succeeded, trigger events
+            if (unpublishResult?.Success ?? false)
             {
-                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);
-                }
+                // events and audit
+                scope.Notifications.Publish(
+                    new ContentUnpublishedNotification(content, eventMessages).WithState(notificationState));
+                scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
 
-                if (content.Published)
+                if (culturesUnpublishing != null)
                 {
-                    // 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)
+                    // 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)
                     {
-                        unpublishResult = StrategyUnpublish(content, eventMessages);
+                        throw new PanicException("publishResult == null - should not happen");
                     }
-                    else
+
+                    switch (publishResult.Result)
                     {
-                        // 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;
+                        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);
                     }
                 }
-                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.");
-                }
-            }
 
-            //Persist the document
-            SaveDocument(content);
+                Audit(AuditType.Unpublish, userId, content.Id);
+                return new PublishResult(PublishResultType.SuccessUnpublish, eventMessages, content);
+            }
 
-            // raise the Saved event, always
-            scope.Notifications.Publish(
-                new ContentSavedNotification(content, eventMessages).WithState(notificationState));
+            // or, failed
+            scope.Notifications.Publish(new ContentTreeChangeNotification(content, changeType, eventMessages));
+            return new PublishResult(PublishResultType.FailedUnpublish, eventMessages, content); // bah
+        }
 
-            if (unpublishing) // we have tried to unpublish - won't happen in a branch
+        // we have tried to publish
+        if (publishing)
+        {
+            // and succeeded, trigger events
+            if (publishResult?.Success ?? false)
             {
-                if (unpublishResult?.Success ?? false) // and succeeded, trigger events
+                if (isNew == false && previouslyPublished == false)
                 {
-                    // events and audit
-                    scope.Notifications.Publish(
-                        new ContentUnpublishedNotification(content, eventMessages).WithState(notificationState));
-                    scope.Notifications.Publish(new ContentTreeChangeNotification(content,
-                        TreeChangeTypes.RefreshBranch, eventMessages));
+                    changeType = TreeChangeTypes.RefreshBranch; // whole branch
+                }
+                else if (isNew == false && previouslyPublished)
+                {
+                    changeType = TreeChangeTypes.RefreshNode; // single node
+                }
 
-                    if (culturesUnpublishing != null)
-                    {
-                        // This will mean that that we unpublished a mandatory culture or we unpublished the last culture.
+                // 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));
+                }
 
-                        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);
+                // 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));
+                }
 
-                        if (publishResult == null)
+                switch (publishResult.Result)
+                {
+                    case PublishResultType.SuccessPublish:
+                        Audit(AuditType.Publish, userId, content.Id);
+                        break;
+                    case PublishResultType.SuccessPublishCulture:
+                        if (culturesPublishing != null)
                         {
-                            throw new PanicException("publishResult == null - should not happen");
+                            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);
                         }
 
-                        switch (publishResult.Result)
+                        break;
+                    case PublishResultType.SuccessUnpublishCulture:
+                        if (culturesUnpublishing != null)
                         {
-                            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);
+                            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);
                         }
-                    }
 
-                    Audit(AuditType.Unpublish, userId, content.Id);
-                    return new PublishResult(PublishResultType.SuccessUnpublish, eventMessages, content);
+                        break;
                 }
 
-                // or, failed
-                scope.Notifications.Publish(new ContentTreeChangeNotification(content, changeType, eventMessages));
-                return new PublishResult(PublishResultType.FailedUnpublish, eventMessages, content); // bah
+                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);
             }
+        }
+
+        // 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();
 
-            if (publishing) // we have tried to publish
+        // 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))
             {
-                if (publishResult?.Success ?? false) // and succeeded, trigger events
+                ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(d.Id);
+                if (d.ContentType.VariesByCulture())
                 {
-                    if (isNew == false && previouslyPublished == false)
-                    {
-                        changeType = TreeChangeTypes.RefreshBranch; // whole branch
-                    }
-                    else if (isNew == false && previouslyPublished)
+                    // find which cultures have pending schedules
+                    var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Expire, date)
+                        .Select(x => x.Culture)
+                        .Distinct()
+                        .ToList();
+
+                    if (pendingCultures.Count == 0)
                     {
-                        changeType = TreeChangeTypes.RefreshNode; // single node
+                        continue; // shouldn't happen but no point in processing this document if there's nothing there
                     }
 
-
-                    // invalidate the node/branch
-                    if (!branchOne) // for branches, handled by SaveAndPublishBranch
+                    var savingNotification = new ContentSavingNotification(d, evtMsgs);
+                    if (scope.Notifications.PublishCancelable(savingNotification))
                     {
-                        scope.Notifications.Publish(
-                            new ContentTreeChangeNotification(content, changeType, eventMessages));
-                        scope.Notifications.Publish(
-                            new ContentPublishedNotification(content, eventMessages).WithState(notificationState));
+                        results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
+                        continue;
                     }
 
-                    // 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))
+                    foreach (var c in pendingCultures)
                     {
-                        IContent[] descendants = GetPublishedDescendantsLocked(content).ToArray();
-                        scope.Notifications.Publish(
-                            new ContentPublishedNotification(descendants, eventMessages).WithState(notificationState));
+                        // Clear this schedule for this culture
+                        contentSchedule.Clear(c, ContentScheduleAction.Expire, date);
+
+                        // set the culture to be published
+                        d.UnpublishCulture(c);
                     }
 
-                    switch (publishResult.Result)
+                    _documentRepository.PersistContentSchedule(d, contentSchedule);
+                    PublishResult result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId);
+                    if (result.Success == false)
                     {
-                        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;
+                        _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
                     }
 
-                    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);
+                    results.Add(result);
                 }
                 else
                 {
-                    Audit(AuditType.Save, userId, content.Id);
+                    // 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);
                 }
             }
 
-            // or, failed
-            scope.Notifications.Publish(new ContentTreeChangeNotification(content, changeType, eventMessages));
-            return publishResult!;
+            _documentRepository.ClearSchedule(date, ContentScheduleAction.Expire);
         }
 
-        /// 
-        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);
+        scope.Complete();
+    }
 
-            return results;
-        }
+    private void PerformScheduledPublishingRelease(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope();
 
-        private void PerformScheduledPublishingExpiration(DateTime date, List results,
-            EventMessages evtMsgs, Lazy> allLangs)
+        // do a fast read without any locks since this executes often to see if we even need to proceed
+        if (_documentRepository.HasContentForRelease(date))
         {
-            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            // now take a write lock since we'll be updating
+            scope.WriteLock(Constants.Locks.ContentTree);
 
-            // do a fast read without any locks since this executes often to see if we even need to proceed
-            if (_documentRepository.HasContentForExpiration(date))
+            foreach (IContent d in _documentRepository.GetContentForRelease(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)
-                        {
-                            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);
-                        }
+                    // find which cultures have pending schedules
+                    var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Release, date)
+                        .Select(x => x.Culture)
+                        .Distinct()
+                        .ToList();
 
-                        results.Add(result);
-                    }
-                    else
+                    if (pendingCultures.Count == 0)
                     {
-                        //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);
+                        continue; // shouldn't happen but no point in processing this document if there's nothing there
                     }
-                }
-
-                _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())
+                    var savingNotification = new ContentSavingNotification(d, evtMsgs);
+                    if (scope.Notifications.PublishCancelable(savingNotification))
                     {
-                        //find which cultures have pending schedules
-                        var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Release, date)
-                            .Select(x => x.Culture)
-                            .Distinct()
-                            .ToList();
+                        results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
+                        continue;
+                    }
 
-                        if (pendingCultures.Count == 0)
-                        {
-                            continue; //shouldn't happen but no point in processing this document if there's nothing there
-                        }
+                    var publishing = true;
+                    foreach (var culture in pendingCultures)
+                    {
+                        // Clear this schedule for this culture
+                        contentSchedule.Clear(culture, ContentScheduleAction.Release, date);
 
-                        var savingNotification = new ContentSavingNotification(d, evtMsgs);
-                        if (scope.Notifications.PublishCancelable(savingNotification))
+                        if (d.Trashed)
                         {
-                            results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
-                            continue;
+                            continue; // won't publish
                         }
 
-                        var publishing = true;
-                        foreach (var culture in pendingCultures)
+                        // 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)
                         {
-                            //Clear this schedule for this culture
-                            contentSchedule.Clear(culture, ContentScheduleAction.Release, date);
-
-                            if (d.Trashed)
-                            {
-                                continue; // won't publish
-                            }
-
-                            //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)));
-                            }
-
-                            publishing &= tryPublish; //set the culture to be published
-                            if (!publishing)
-                            {
-                                continue;
-                            }
+                            _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)));
                         }
 
-                        PublishResult result;
-
-                        if (d.Trashed)
-                        {
-                            result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
-                        }
-                        else if (!publishing)
-                        {
-                            result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d);
-                        }
-                        else
+                        publishing &= tryPublish; // set the culture to be published
+                        if (!publishing)
                         {
-                            _documentRepository.PersistContentSchedule(d, contentSchedule);
-                            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);
-                        }
+                    PublishResult result;
 
-                        results.Add(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);
+                        _documentRepository.PersistContentSchedule(d, contentSchedule);
+                        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);
+                    }
 
-                        PublishResult? result = null;
+                    results.Add(result);
+                }
+                else
+                {
+                    // Clear this schedule
+                    contentSchedule.Clear(ContentScheduleAction.Release, date);
 
-                        if (d.Trashed)
-                        {
-                            result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
-                        }
-                        else
-                        {
-                            _documentRepository.PersistContentSchedule(d, contentSchedule);
-                            result = SaveAndPublish(d, userId: d.WriterId);
-                        }
+                    PublishResult? result = null;
 
-                        if (result.Success == false)
-                        {
-                            _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id,
-                                result.Result);
-                        }
+                    if (d.Trashed)
+                    {
+                        result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
+                    }
+                    else
+                    {
+                        _documentRepository.PersistContentSchedule(d, contentSchedule);
+                        result = SaveAndPublish(d, userId: d.WriterId);
+                    }
 
-                        results.Add(result);
+                    if (result.Success == false)
+                    {
+                        _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
                     }
-                }
 
-                _documentRepository.ClearSchedule(date, ContentScheduleAction.Release);
+                    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)
-        {
-            //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 =>
-                {
-                    var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content);
-                    return content.PublishCulture(impact) &&
-                           _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact);
-                });
-            }
+        scope.Complete();
+    }
 
-            return content.PublishCulture(CultureImpact.Invariant)
-                   && _propertyValidationService.Value.IsPropertyDataValid(content, out _, CultureImpact.Invariant);
-        }
+    // 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
 
-        // utility 'ShouldPublish' func used by SaveAndPublishBranch
-        private HashSet? SaveAndPublishBranch_ShouldPublish(ref HashSet? cultures, string c,
-            bool published, bool edited, bool isRoot, bool force)
+        // variant content type - publish specified cultures
+        // invariant content type - publish only the invariant culture
+        if (content.ContentType.VariesByCulture())
         {
-            // if published, republish
-            if (published)
+            return culturesToPublish.All(culture =>
             {
-                if (cultures == null)
-                {
-                    cultures = new HashSet(); // empty means 'already published'
-                }
-
-                if (edited)
-                {
-                    cultures.Add(c); //  means 'republish this culture'
-                }
+                var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content);
+                return content.PublishCulture(impact) &&
+                       _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact);
+            });
+        }
 
-                return cultures;
-            }
+        return content.PublishCulture(CultureImpact.Invariant)
+               && _propertyValidationService.Value.IsPropertyDataValid(content, out _, CultureImpact.Invariant);
+    }
 
-            // if not published, publish if force/root else do nothing
-            if (!force && !isRoot)
+    // 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 (cultures == null)
             {
-                return cultures; // null means 'nothing to do'
+                cultures = new HashSet(); // empty means 'already published'
             }
 
-            if (cultures == null)
+            if (edited)
             {
-                cultures = new HashSet();
+                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
+            return cultures; // null means 'nothing to do'
+        }
 
-            // 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).
+        if (cultures == null)
+        {
+            cultures = new HashSet();
+        }
 
-            // 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;
+        cultures.Add(c); //  means 'publish this culture'
+        return cultures;
+    }
 
-                if (!c.ContentType.VariesByCulture()) // invariant content type
-                {
-                    return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot,
-                        force);
-                }
+    /// 
+    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
 
-                if (culture != "*") // variant content type, specific culture
-                {
-                    return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture,
-                        c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force);
-                }
+        // 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).
 
-                // 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);
-                    }
+        // 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 culture
+            if (culture != "*")
+            {
+                return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force);
+            }
 
-                    return culturesToPublish;
+            // 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);
                 }
 
-                // 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 culturesToPublish;
             }
 
-            return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
+            // if not published, publish if force/root else do nothing
+            return force || isRoot
+                ? new HashSet { "*" } // "*" means 'publish all'
+                : null; // null means 'nothing to do'
         }
 
-        /// 
-        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
+        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();
 
-            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;
 
-            // 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)
+            // invariant content type
+            if (!c.ContentType.VariesByCulture())
             {
-                var isRoot = c.Id == content.Id;
-                HashSet? culturesToPublish = null;
+                return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
+            }
 
-                if (!c.ContentType.VariesByCulture()) // invariant content type
+            // 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)
                 {
-                    return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot,
-                        force);
+                    SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), 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;
+            }
 
-                    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'
+        }
 
-                // 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);
+    }
 
-            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));
         }
 
-        internal IEnumerable SaveAndPublishBranch(IContent document, bool force,
-            Func?> shouldPublish,
-            Func, IReadOnlyCollection, bool> publishCultures,
-            int userId = Constants.Security.SuperUserId)
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        var results = new List();
+        var publishedDocuments = new List();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            if (shouldPublish == null)
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var allLangs = _languageRepository.GetMany().ToList();
+
+            if (!document.HasIdentity)
             {
-                throw new ArgumentNullException(nameof(shouldPublish));
+                throw new InvalidOperationException("Cannot not branch-publish a new document.");
             }
 
-            if (publishCultures == null)
+            PublishedState publishedState = document.PublishedState;
+            if (publishedState == PublishedState.Publishing)
             {
-                throw new ArgumentNullException(nameof(publishCultures));
+                throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch.");
             }
 
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            var results = new List();
-            var publishedDocuments = new List();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            // deal with the branch root - if it fails, abort
+            PublishResult? result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs);
+            if (result != null)
             {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var allLangs = _languageRepository.GetMany().ToList();
-
-                if (!document.HasIdentity)
+                results.Add(result);
+                if (!result.Success)
                 {
-                    throw new InvalidOperationException("Cannot not branch-publish a new document.");
+                    return results;
                 }
+            }
 
-                PublishedState publishedState = document.PublishedState;
-                if (publishedState == PublishedState.Publishing)
-                {
-                    throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch.");
-                }
+            // 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;
 
-                // deal with the branch root - if it fails, abort
-                PublishResult? result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true,
-                    publishedDocuments, eventMessages, userId, allLangs);
-                if (result != null)
+                // 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)))
                 {
-                    results.Add(result);
-                    if (!result.Success)
+                    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);
+                }
 
-                        // if we could not publish the document, cut its branch
-                        exclude.Add(d.Id);
-                    }
+                page++;
+            }
+            while (count > 0);
 
-                    page++;
-                } while (count > 0);
+            Audit(AuditType.Publish, userId, document.Id, "Branch published");
 
-                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));
 
-                // 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();
+        }
 
-                scope.Complete();
-            }
+        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);
 
-            return results;
+        // null = do not include
+        if (culturesToPublish == null)
+        {
+            return null;
         }
 
-        // 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)
+        // empty = already published
+        if (culturesToPublish.Count == 0)
         {
-            HashSet? culturesToPublish = shouldPublish(document);
-            if (culturesToPublish == null) // null = do not include
-            {
-                return null;
-            }
+            return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document);
+        }
 
-            if (culturesToPublish.Count == 0) // empty = already published
-            {
-                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);
+        }
 
-            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
 
-            // publish & check if values are valid
-            if (!publishCultures(document, culturesToPublish, allLangs))
+    /// 
+    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)))
             {
-                //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);
+                scope.Complete();
+                return OperationResult.Cancel(eventMessages);
             }
 
-            PublishResult result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs,
-                savingNotification.State, userId, true, isRoot);
-            if (result.Success)
+            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)
             {
-                publishedDocuments.Add(document);
+                scope.Notifications.Publish(new ContentUnpublishedNotification(content, eventMessages));
             }
 
-            return result;
-        }
+            DeleteLocked(scope, content, eventMessages);
 
-        #endregion
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(content, TreeChangeTypes.Remove, eventMessages));
+            Audit(AuditType.Delete, userId, content.Id);
+
+            scope.Complete();
+        }
 
-        #region Delete
+        return OperationResult.Succeed(eventMessages);
+    }
 
-        /// 
-        public OperationResult Delete(IContent content, int userId = Constants.Security.SuperUserId)
+    private void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs)
+    {
+        void DoDelete(IContent c)
         {
-            EventMessages eventMessages = EventMessagesFactory.Get();
+            _documentRepository.Delete(c);
+            scope.Notifications.Publish(new ContentDeletedNotification(c, evtMsgs));
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                if (scope.Notifications.PublishCancelable(new ContentDeletingNotification(content, eventMessages)))
-                {
-                    scope.Complete();
-                    return OperationResult.Cancel(eventMessages);
-                }
+            // media files deleted by QueuingEventDispatcher
+        }
 
-                scope.WriteLock(Constants.Locks.ContentTree);
+        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);
+            }
+        }
 
-                // 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));
-                }
+        DoDelete(content);
+    }
 
-                DeleteLocked(scope, content, eventMessages);
+    // 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.
 
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(content, TreeChangeTypes.Remove, eventMessages));
-                Audit(AuditType.Delete, userId, content.Id);
+    /// 
+    ///     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;
             }
 
-            return OperationResult.Succeed(eventMessages);
+            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();
 
-        private void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            void DoDelete(IContent c)
+            var deletingVersionsNotification = new ContentDeletingVersionsNotification(id, evtMsgs, versionId);
+            if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
             {
-                _documentRepository.Delete(c);
-                scope.Notifications.Publish(new ContentDeletedNotification(c, evtMsgs));
+                scope.Complete();
+                return;
+            }
 
-                // media files deleted by QueuingEventDispatcher
+            if (deletePriorVersions)
+            {
+                IContent? content = GetVersion(versionId);
+                DeleteVersions(id, content?.UpdateDate ?? DateTime.Now, userId);
             }
 
-            const int pageSize = 500;
-            var total = long.MaxValue;
-            while (total > 0)
+            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)
             {
-                //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);
-                }
+                _documentRepository.DeleteVersion(versionId);
             }
 
-            DoDelete(content);
+            scope.Notifications.Publish(
+                new ContentDeletedVersionsNotification(id, evtMsgs, versionId).WithStateFrom(
+                    deletingVersionsNotification));
+            Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version)");
+
+            scope.Complete();
         }
+    }
 
-        //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.
+    #endregion
 
-        /// 
-        ///     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();
+    #region Move, RecycleBin
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var deletingVersionsNotification =
-                    new ContentDeletingVersionsNotification(id, evtMsgs, dateToRetain: versionDate);
-                if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
+    /// 
+    public OperationResult MoveToRecycleBin(IContent content, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        var moves = new List<(IContent, string)>();
 
-                scope.WriteLock(Constants.Locks.ContentTree);
-                _documentRepository.DeleteVersions(id, versionDate);
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
 
-                scope.Notifications.Publish(
-                    new ContentDeletedVersionsNotification(id, evtMsgs, dateToRetain: versionDate).WithStateFrom(
-                        deletingVersionsNotification));
-                Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version date)");
+            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
             }
-        }
 
-        /// 
-        ///     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();
+            // 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));
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var deletingVersionsNotification = new ContentDeletingVersionsNotification(id, evtMsgs, versionId);
-                if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
+            MoveEventInfo[] moveInfo = moves
+                .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
+                .ToArray();
 
-                if (deletePriorVersions)
-                {
-                    IContent? content = GetVersion(versionId);
-                    DeleteVersions(id, content?.UpdateDate ?? DateTime.Now, userId);
-                }
+            scope.Notifications.Publish(
+                new ContentMovedToRecycleBinNotification(moveInfo, eventMessages).WithStateFrom(
+                    movingToRecycleBinNotification));
+            Audit(AuditType.Move, userId, content.Id, "Moved to recycle bin");
 
-                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.Complete();
+        }
 
-                scope.Notifications.Publish(
-                    new ContentDeletedVersionsNotification(id, evtMsgs, versionId).WithStateFrom(
-                        deletingVersionsNotification));
-                Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version)");
+        return OperationResult.Succeed(eventMessages);
+    }
 
-                scope.Complete();
-            }
+    /// 
+    ///     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;
         }
 
-        #endregion
+        EventMessages eventMessages = EventMessagesFactory.Get();
 
-        #region Move, RecycleBin
+        var moves = new List<(IContent, string)>();
 
-        /// 
-        public OperationResult MoveToRecycleBin(IContent content, int userId = Constants.Security.SuperUserId)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            var moves = new List<(IContent, string)>();
+            scope.WriteLock(Constants.Locks.ContentTree);
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            IContent? parent = parentId == Constants.System.Root ? null : GetById(parentId);
+            if (parentId != Constants.System.Root && (parent == null || parent.Trashed))
             {
-                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();
+                throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback
+            }
 
-                scope.Notifications.Publish(
-                    new ContentMovedToRecycleBinNotification(moveInfo, eventMessages).WithStateFrom(
-                        movingToRecycleBinNotification));
-                Audit(AuditType.Move, userId, content.Id, "Moved to recycle bin");
+            var moveEventInfo = new MoveEventInfo(content, content.Path, parentId);
 
+            var movingNotification = new ContentMovingNotification(moveEventInfo, eventMessages);
+            if (scope.Notifications.PublishCancelable(movingNotification))
+            {
                 scope.Complete();
+                return; // causes rollback
             }
 
-            return OperationResult.Succeed(eventMessages);
-        }
+            // 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;
 
-        /// 
-        ///     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)
+            // 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)
             {
-                MoveToRecycleBin(content, userId);
-                return;
+                // 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;
             }
 
-            EventMessages eventMessages = EventMessagesFactory.Get();
+            PerformMoveLocked(content, parentId, parent, userId, moves, trashed);
 
-            var moves = new List<(IContent, string)>();
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
+            // changes
+            MoveEventInfo[] moveInfo = moves
+                .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
+                .ToArray();
 
-                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
-                }
+            scope.Notifications.Publish(
+                new ContentMovedNotification(moveInfo, eventMessages).WithStateFrom(movingNotification));
+
+            Audit(AuditType.Move, userId, content.Id);
+
+            scope.Complete();
+        }
+    }
 
-                var moveEventInfo = new MoveEventInfo(content, content.Path, parentId);
+    // 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;
 
-                var movingNotification = new ContentMovingNotification(moveEventInfo, eventMessages);
-                if (scope.Notifications.PublishCancelable(movingNotification))
-                {
-                    scope.Complete();
-                    return; // causes rollback
-                }
+        // 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);
 
-                // 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;
+        var paths = new Dictionary();
 
-                // 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;
-                }
+        moves.Add((content, content.Path)); // capture original path
 
-                PerformMoveLocked(content, parentId, parent, userId, moves, trashed);
+        // need to store the original path to lookup descendants based on it below
+        var originalPath = content.Path;
 
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
+        // 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);
 
-                MoveEventInfo[] moveInfo = moves //changes
-                    .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
-                    .ToArray();
+        // 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;
 
-                scope.Notifications.Publish(
-                    new ContentMovedNotification(moveInfo, eventMessages).WithStateFrom(movingNotification));
+        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"));
 
-                Audit(AuditType.Move, userId, content.Id);
+            foreach (IContent descendant in descendants)
+            {
+                moves.Add((descendant, descendant.Path)); // capture original path
 
-                scope.Complete();
+                // 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);
+    }
 
-        // 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)
+    private void PerformMoveContentLocked(IContent content, int userId, bool? trash)
+    {
+        if (trash.HasValue)
         {
-            content.WriterId = userId;
-            content.ParentId = parentId;
+            ((ContentBase)content).Trashed = trash.Value;
+        }
 
-            // 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);
+        content.WriterId = userId;
+        _documentRepository.Save(content);
+    }
 
-            var paths = new Dictionary();
+    /// 
+    ///     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();
 
-            moves.Add((content, content.Path)); // capture original path
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
 
-            //need to store the original path to lookup descendants based on it below
-            var originalPath = content.Path;
+            // 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();
 
-            // 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
+            var emptyingRecycleBinNotification = new ContentEmptyingRecycleBinNotification(contents, eventMessages);
+            if (scope.Notifications.PublishCancelable(emptyingRecycleBinNotification))
             {
-                // 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"));
+                scope.Complete();
+                return OperationResult.Cancel(eventMessages);
+            }
 
-                foreach (IContent descendant in descendants)
+            if (contents is not null)
+            {
+                foreach (IContent content in contents)
                 {
-                    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);
+                    DeleteLocked(scope, content, eventMessages);
+                    deleted.Add(content);
                 }
-            } 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);
+            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();
         }
 
-        /// 
-        ///     Empties the Recycle Bin by deleting all  that resides in the bin
-        /// 
-        public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId)
+        return OperationResult.Succeed(eventMessages);
+    }
+
+    public bool RecycleBinSmells()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            var deleted = new List();
-            EventMessages eventMessages = EventMessagesFactory.Get();
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.RecycleBinSmells();
+        }
+    }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
+    #endregion
 
-                // 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();
+    #region Others
 
-                var emptyingRecycleBinNotification = new ContentEmptyingRecycleBinNotification(contents, eventMessages);
-                if (scope.Notifications.PublishCancelable(emptyingRecycleBinNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Cancel(eventMessages);
-                }
+    /// 
+    ///     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);
 
-                if (contents is not null)
-                {
-                    foreach (IContent content in contents)
-                    {
-                        DeleteLocked(scope, content, eventMessages);
-                        deleted.Add(content);
-                    }
-                }
+    /// 
+    ///     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();
 
-                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");
+        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;
             }
 
-            return OperationResult.Succeed(eventMessages);
-        }
+            // 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>();
 
-        public bool RecycleBinSmells()
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            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)
             {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.RecycleBinSmells();
+                copy.Published = false;
             }
-        }
-
-        #endregion
 
-        #region Others
+            copy.CreatorId = userId;
+            copy.WriterId = 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. 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();
+            // get the current permissions, if there are any explicit ones they need to be copied
+            EntityPermissionCollection currentPermissions = GetPermissions(content);
+            currentPermissions.RemoveWhere(p => p.IsDefaultPermissions);
 
-            IContent copy = content.DeepCloneWithResetIdentities();
-            copy.ParentId = parentId;
+            // save and flush because we need the ID for the recursive Copying events
+            _documentRepository.Save(copy);
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            // add permissions
+            if (currentPermissions.Count > 0)
             {
-                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>();
+                var permissionSet = new ContentPermissionSet(copy, currentPermissions);
+                _documentRepository.AddOrUpdatePermissions(permissionSet);
+            }
 
-                scope.WriteLock(Constants.Locks.ContentTree);
+            // keep track of copies
+            copies.Add(Tuple.Create(content, copy));
+            var idmap = new Dictionary { [content.Id] = copy.Id };
 
-                // a copy is not published (but not really unpublishing either)
-                // update the create author and last edit author
-                if (copy.Published)
+            // process descendants
+            if (recursive)
+            {
+                const int pageSize = 500;
+                var page = 0;
+                var total = long.MaxValue;
+                while (page * pageSize < total)
                 {
-                    copy.Published = false;
-                }
+                    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;
+                        }
 
-                copy.CreatorId = userId;
-                copy.WriterId = userId;
+                        IContent descendantCopy = descendant.DeepCloneWithResetIdentities();
+                        descendantCopy.ParentId = parentId;
 
-                //get the current permissions, if there are any explicit ones they need to be copied
-                EntityPermissionCollection currentPermissions = GetPermissions(content);
-                currentPermissions.RemoveWhere(p => p.IsDefaultPermissions);
+                        if (scope.Notifications.PublishCancelable(
+                                new ContentCopyingNotification(descendant, descendantCopy, parentId, eventMessages)))
+                        {
+                            continue;
+                        }
 
-                // save and flush because we need the ID for the recursive Copying events
-                _documentRepository.Save(copy);
+                        // a copy is not published (but not really unpublishing either)
+                        // update the create author and last edit author
+                        if (descendantCopy.Published)
+                        {
+                            descendantCopy.Published = false;
+                        }
 
-                //add permissions
-                if (currentPermissions.Count > 0)
-                {
-                    var permissionSet = new ContentPermissionSet(copy, currentPermissions);
-                    _documentRepository.AddOrUpdatePermissions(permissionSet);
-                }
+                        descendantCopy.CreatorId = userId;
+                        descendantCopy.WriterId = userId;
 
-                // keep track of copies
-                copies.Add(Tuple.Create(content, copy));
-                var idmap = new Dictionary {[content.Id] = copy.Id};
+                        // save and flush (see above)
+                        _documentRepository.Save(descendantCopy);
 
-                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;
-                        }
+                        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
+            // 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));
+            }
 
-                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);
 
-                Audit(AuditType.Copy, userId, content.Id);
+            scope.Complete();
+        }
 
-                scope.Complete();
-            }
+        return copy;
+    }
 
-            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;
         }
 
-        /// 
-        ///     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)
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            if (content is null)
+            var sendingToPublishNotification = new ContentSendingToPublishNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(sendingToPublishNotification))
             {
+                scope.Complete();
                 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;
 
-                //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.
 
-                // 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);
 
-                //Save before raising event
-                OperationResult saveResult = Save(content, userId);
+            // always complete (but maybe return a failed status)
+            scope.Complete();
 
-                // always complete (but maybe return a failed status)
-                scope.Complete();
+            if (!saveResult.Success)
+            {
+                return saveResult.Success;
+            }
 
-                if (!saveResult.Success)
-                {
-                    return saveResult.Success;
-                }
+            scope.Notifications.Publish(
+                new ContentSentToPublishNotification(content, evtMsgs).WithStateFrom(sendingToPublishNotification));
 
-                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);
+            }
 
-                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;
+        }
+    }
 
-                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);
         }
 
-        /// 
-        ///     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)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
+            scope.WriteLock(Constants.Locks.ContentTree);
 
-            IContent[] itemsA = items.ToArray();
-            if (itemsA.Length == 0)
-            {
-                return new OperationResult(OperationResultType.NoOperation, evtMsgs);
-            }
+            OperationResult ret = Sort(scope, itemsA, userId, evtMsgs);
+            scope.Complete();
+            return ret;
+        }
+    }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
+    /// 
+    ///     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();
 
-                OperationResult ret = Sort(scope, itemsA, userId, evtMsgs);
-                scope.Complete();
-                return ret;
-            }
+        var idsA = ids?.ToArray();
+        if (idsA is null || idsA.Length == 0)
+        {
+            return new OperationResult(OperationResultType.NoOperation, evtMsgs);
         }
 
-        /// 
-        ///     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)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
+            scope.WriteLock(Constants.Locks.ContentTree);
+            IContent[] itemsA = GetByIds(idsA).ToArray();
 
-            var idsA = ids?.ToArray();
-            if (idsA is null || idsA.Length == 0)
-            {
-                return new OperationResult(OperationResultType.NoOperation, evtMsgs);
-            }
+            OperationResult ret = Sort(scope, itemsA, userId, evtMsgs);
+            scope.Complete();
+            return ret;
+        }
+    }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-                IContent[] itemsA = GetByIds(idsA).ToArray();
+    private OperationResult Sort(ICoreScope scope, IContent[] itemsA, int userId, EventMessages eventMessages)
+    {
+        var sortingNotification = new ContentSortingNotification(itemsA, eventMessages);
+        var savingNotification = new ContentSavingNotification(itemsA, eventMessages);
 
-                OperationResult ret = Sort(scope, itemsA, userId, evtMsgs);
-                scope.Complete();
-                return ret;
-            }
+        // raise cancelable sorting event
+        if (scope.Notifications.PublishCancelable(sortingNotification))
+        {
+            return OperationResult.Cancel(eventMessages);
         }
 
-        private OperationResult Sort(ICoreScope scope, IContent[] itemsA, int userId, EventMessages eventMessages)
+        // raise cancelable saving event
+        if (scope.Notifications.PublishCancelable(savingNotification))
         {
-            var sortingNotification = new ContentSortingNotification(itemsA, eventMessages);
-            var savingNotification = new ContentSavingNotification(itemsA, eventMessages);
+            return OperationResult.Cancel(eventMessages);
+        }
+
+        var published = new List();
+        var saved = new List();
+        var sortOrder = 0;
 
-            // raise cancelable sorting event
-            if (scope.Notifications.PublishCancelable(sortingNotification))
+        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)
             {
-                return OperationResult.Cancel(eventMessages);
+                sortOrder++;
+                continue;
             }
 
-            // raise cancelable saving event
-            if (scope.Notifications.PublishCancelable(savingNotification))
+            // 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)
             {
-                return OperationResult.Cancel(eventMessages);
+                published.Add(content);
             }
 
-            var published = new List();
-            var saved = new List();
-            var sortOrder = 0;
+            // save
+            saved.Add(content);
+            _documentRepository.Save(content);
+        }
 
-            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;
-                }
+        // first saved, then sorted
+        scope.Notifications.Publish(
+            new ContentSavedNotification(itemsA, eventMessages).WithStateFrom(savingNotification));
+        scope.Notifications.Publish(
+            new ContentSortedNotification(itemsA, eventMessages).WithStateFrom(sortingNotification));
 
-                // else update
-                content.SortOrder = sortOrder++;
-                content.WriterId = userId;
+        scope.Notifications.Publish(
+            new ContentTreeChangeNotification(saved, TreeChangeTypes.RefreshNode, eventMessages));
 
-                // 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);
-                }
+        if (published.Any())
+        {
+            scope.Notifications.Publish(new ContentPublishedNotification(published, eventMessages));
+        }
 
-                // save
-                saved.Add(content);
-                _documentRepository.Save(content);
-            }
+        Audit(AuditType.Sort, userId, 0, "Sorting content performed by user");
+        return OperationResult.Succeed(eventMessages);
+    }
 
-            //first saved, then sorted
-            scope.Notifications.Publish(
-                new ContentSavedNotification(itemsA, eventMessages).WithStateFrom(savingNotification));
-            scope.Notifications.Publish(
-                new ContentSortedNotification(itemsA, eventMessages).WithStateFrom(sortingNotification));
+    public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
 
-            scope.Notifications.Publish(
-                new ContentTreeChangeNotification(saved, TreeChangeTypes.RefreshNode, eventMessages));
+            ContentDataIntegrityReport report = _documentRepository.CheckDataIntegrity(options);
 
-            if (published.Any())
+            if (report.FixedIssues.Count > 0)
             {
-                scope.Notifications.Publish(new ContentPublishedNotification(published, eventMessages));
+                // 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()));
             }
 
-            Audit(AuditType.Sort, userId, 0, "Sorting content performed by user");
-            return OperationResult.Succeed(eventMessages);
+            return report;
         }
+    }
+
+    #endregion
 
-        public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
+    #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))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return GetPublishedDescendantsLocked(content).ToArray(); // ToArray important in uow!
+        }
+    }
 
-                ContentDataIntegrityReport report = _documentRepository.CheckDataIntegrity(options);
+    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);
 
-                if (report.FixedIssues.Count > 0)
+        // 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))
                 {
-                    //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()));
+                    yield return c;
+                    parents.Add(c.Id);
                 }
-
-                return report;
             }
         }
+    }
+
+    #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
+    #endregion
 
-        #region Internal Methods
+    #region Publishing Strategies
 
-        /// 
-        ///     Gets a collection of  descendants by the first Parent.
-        /// 
-        ///  item to retrieve Descendants from
-        /// An Enumerable list of  objects
-        internal IEnumerable GetPublishedDescendants(IContent content)
+    /// 
+    ///     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)))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return GetPublishedDescendantsLocked(content).ToArray(); // ToArray important in uow!
-            }
+            _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled");
+            return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
         }
 
-        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);
+        var variesByCulture = content.ContentType.VariesByCulture();
 
-            // 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
+        // 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();
 
-            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);
-                    }
-                }
-            }
+        // publish the culture(s)
+        if (!impactsToPublish.All(content.PublishCulture))
+        {
+            return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
         }
 
-        #endregion
+        // 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,
+            };
+        }
 
-        #region Private Methods
+        // 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.");
+            }
 
-        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));
+            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);
+            }
 
-        private bool IsDefaultCulture(IReadOnlyCollection? langs, string culture) =>
-            langs?.Any(x => x.IsDefault && x.IsoCode.InvariantEquals(culture)) ?? false;
+            // 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);
+            }
 
-        private bool IsMandatoryCulture(IReadOnlyCollection langs, string culture) =>
-            langs.Any(x => x.IsMandatory && x.IsoCode.InvariantEquals(culture));
+            if (culturesPublishing.Count == 0 && culturesUnpublishing?.Count > 0)
+            {
+                return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content);
+            }
+        }
 
-        #endregion
+        // 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);
+        }
 
-        #region Publishing Strategies
+        ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(content.Id);
 
-        /// 
-        ///     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)
+        // loop over each culture publishing - or string.Empty for invariant
+        foreach (var culture in culturesPublishing ?? new[] { string.Empty })
         {
-            // raise Publishing notification
-            if (scope.Notifications.PublishCancelable(
-                    new ContentPublishingNotification(content, evtMsgs).WithState(notificationState)))
+            // ensure that the document status is correct
+            // note: culture will be string.Empty for invariant
+            switch (content.GetStatus(contentSchedule, culture))
             {
-                _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
-                    content.Name, content.Id, "publishing was cancelled");
-                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
-            }
+                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);
 
-            var variesByCulture = content.ContentType.VariesByCulture();
+                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");
+                    }
 
-            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();
+                    return new PublishResult(
+                        !variesByCulture
+                            ? PublishResultType.FailedPublishAwaitingRelease
+                            : PublishResultType.FailedPublishCultureAwaitingRelease,
+                        evtMsgs,
+                        content);
 
-            // publish the culture(s)
-            if (!impactsToPublish.All(content.PublishCulture))
-            {
-                return new PublishResult(PublishResultType.FailedPublishContentInvalid, 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);
             }
+        }
 
-            //validate the property values
-            IProperty[]? invalidProperties = null;
-            if (!impactsToPublish.All(x =>
-                    _propertyValidationService.Value.IsPropertyDataValid(content, out invalidProperties, x)))
+        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)
             {
-                return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content)
-                {
-                    InvalidProperties = invalidProperties
-                };
+                _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);
             }
+        }
 
-            //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);
-                }
-
+        // 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);
+        }
 
-                // 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);
-                }
+        return new PublishResult(evtMsgs, content);
+    }
 
-                if (culturesPublishing.Count == 0 && culturesUnpublishing?.Count > 0)
-                {
-                    return new PublishResult(PublishResultType.SuccessUnpublishCulture, 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;
 
-            // 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)
+        // 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)
             {
-                _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})
+            if (culturesUnpublishing?.Count > 0)
             {
-                // 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);
-                }
+                _logger.LogInformation(
+                    "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been unpublished.",
+                    content.Name,
+                    content.Id,
+                    string.Join(",", culturesUnpublishing));
             }
 
-            if (checkPath)
+            if (culturesPublishing?.Count > 0)
             {
-                // 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);
-                }
+                _logger.LogInformation(
+                    "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been published.",
+                    content.Name,
+                    content.Id,
+                    string.Join(",", culturesPublishing));
             }
 
-            //If we are both publishing and unpublishing cultures, then return a mixed status
-            if (variesByCulture && culturesPublishing?.Count > 0 && culturesUnpublishing?.Count > 0)
+            if (culturesUnpublishing?.Count > 0 && culturesPublishing?.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 (culturesUnpublishing?.Count > 0 && culturesPublishing?.Count == 0)
             {
-                if (content.Published && culturesUnpublishing?.Count == 0 && culturesPublishing?.Count == 0)
-                {
-                    return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
-                }
+                return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content);
+            }
 
-                if (culturesUnpublishing?.Count > 0)
-                {
-                    _logger.LogInformation(
-                        "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been unpublished.",
-                        content.Name, content.Id, string.Join(",", culturesUnpublishing));
-                }
+            return new PublishResult(PublishResultType.SuccessPublishCulture, evtMsgs, content);
+        }
 
-                if (culturesPublishing?.Count > 0)
-                {
-                    _logger.LogInformation(
-                        "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been published.",
-                        content.Name, content.Id, string.Join(",", culturesPublishing));
-                }
+        _logger.LogInformation("Document {ContentName} (id={ContentId}) has been published.", content.Name, content.Id);
+        return new PublishResult(evtMsgs, content);
+    }
 
-                if (culturesUnpublishing?.Count > 0 && culturesPublishing?.Count > 0)
-                {
-                    return new PublishResult(PublishResultType.SuccessMixedCulture, 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);
+        }
 
-                if (culturesUnpublishing?.Count > 0 && culturesPublishing?.Count == 0)
-                {
-                    return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content);
-                }
+        return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content);
+    }
 
-                return new PublishResult(PublishResultType.SuccessPublishCulture, 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);
 
-            _logger.LogInformation("Document {ContentName} (id={ContentId}) has been published.", content.Name,
-                content.Id);
-            return new PublishResult(evtMsgs, content);
+        // TODO: What is this check?? we just created this attempt and of course it is Success?!
+        if (attempt.Success == false)
+        {
+            return attempt;
         }
 
-        /// 
-        ///     Ensures that a document can be unpublished
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        private PublishResult StrategyCanUnpublish(ICoreScope scope, IContent content, EventMessages evtMsgs)
+        // 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)
         {
-            // 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);
+            contentSchedule.Remove(p);
         }
 
-        /// 
-        ///     Unpublishes a document
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        ///     It is assumed that all unpublishing checks have passed before calling this method like
-        ///     
-        /// 
-        private PublishResult StrategyUnpublish(IContent content, EventMessages evtMsgs)
+        if (pastReleases.Count > 0)
         {
-            var attempt = new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content);
+            _logger.LogInformation(
+                "Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.", content.Name, content.Id);
+        }
 
-            //TODO: What is this check?? we just created this attempt and of course it is Success?!
-            if (attempt.Success == false)
-            {
-                return attempt;
-            }
+        _documentRepository.PersistContentSchedule(content, contentSchedule);
 
-            // 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
+        // change state to unpublishing
+        content.PublishedState = PublishedState.Unpublishing;
 
-            var contentSchedule = _documentRepository.GetContentSchedule(content.Id);
-            var pastReleases = contentSchedule.GetPending(ContentScheduleAction.Expire, DateTime.Now);
-            foreach (var p in pastReleases)
-                contentSchedule.Remove(p);
+        _logger.LogInformation("Document {ContentName} (id={ContentId}) has been unpublished.", content.Name, content.Id);
+        return attempt;
+    }
 
-            if (pastReleases.Count > 0)
-            {
-                _logger.LogInformation(
-                    "Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.",
-                    content.Name, content.Id);
-            }
+    #endregion
 
-            _documentRepository.PersistContentSchedule(content, contentSchedule);
-            // change state to unpublishing
-            content.PublishedState = PublishedState.Unpublishing;
+    #region Content Types
 
-            _logger.LogInformation("Document {ContentName} (id={ContentId}) has been unpublished.", content.Name,
-                content.Id);
-            return attempt;
-        }
+    /// 
+    ///     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();
 
-        #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);
 
-            // 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())
+            IQuery query = Query().WhereIn(x => x.ContentTypeId, contentTypeIdsA);
+            IContent[] contents = _documentRepository.Get(query).ToArray();
+
+            if (contents is null)
             {
-                scope.WriteLock(Constants.Locks.ContentTree);
+                return;
+            }
 
-                IQuery query = Query().WhereIn(x => x.ContentTypeId, contentTypeIdsA);
-                IContent[] contents = _documentRepository.Get(query).ToArray();
+            if (scope.Notifications.PublishCancelable(new ContentDeletingNotification(contents, eventMessages)))
+            {
+                scope.Complete();
+                return;
+            }
 
-                if (contents is null)
+            // 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)
                 {
-                    return;
+                    scope.Notifications.Publish(new ContentUnpublishedNotification(content, eventMessages));
                 }
 
-                if (scope.Notifications.PublishCancelable(new ContentDeletingNotification(contents, 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)
                 {
-                    scope.Complete();
-                    return;
+                    // see MoveToRecycleBin
+                    PerformMoveLocked(child, Constants.System.RecycleBinContent, null, userId, moves, true);
+                    changes.Add(new TreeChange(content, TreeChangeTypes.RefreshBranch));
                 }
 
-                // 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));
-                    }
+                // delete content
+                // triggers the deleted event (and handles the files)
+                DeleteLocked(scope, content, eventMessages);
+                changes.Add(new TreeChange(content, TreeChangeTypes.Remove));
+            }
 
-                    // 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));
-                    }
+            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));
+            }
 
-                    // delete content
-                    // triggers the deleted event (and handles the files)
-                    DeleteLocked(scope, content, eventMessages);
-                    changes.Add(new TreeChange(content, TreeChangeTypes.Remove));
-                }
+            scope.Notifications.Publish(new ContentTreeChangeNotification(changes, 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));
-                }
+            Audit(AuditType.Delete, userId, Constants.System.Root, $"Delete content of type {string.Join(",", contentTypeIdsA)}");
 
-                scope.Notifications.Publish(new ContentTreeChangeNotification(changes, eventMessages));
+            scope.Complete();
+        }
+    }
 
-                Audit(AuditType.Delete, userId, Constants.System.Root,
-                    $"Delete content of type {string.Join(",", contentTypeIdsA)}");
+    /// 
+    ///     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);
 
-                scope.Complete();
-            }
+    private IContentType GetContentType(ICoreScope scope, string contentTypeAlias)
+    {
+        if (contentTypeAlias == null)
+        {
+            throw new ArgumentNullException(nameof(contentTypeAlias));
         }
 
-        /// 
-        ///     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 (string.IsNullOrWhiteSpace(contentTypeAlias))
         {
-            if (contentTypeAlias == null)
-            {
-                throw new ArgumentNullException(nameof(contentTypeAlias));
-            }
+            throw new ArgumentException("Value can't be empty or consist only of white-space characters.", 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);
 
-            scope.ReadLock(Constants.Locks.ContentTypes);
+        IQuery query = Query().Where(x => x.Alias == contentTypeAlias);
+        IContentType? contentType = _contentTypeRepository.Get(query).FirstOrDefault();
 
-            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
+        }
 
-            if (contentType == null)
-            {
-                throw new Exception(
-                    $"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback
-            }
+        return contentType;
+    }
 
-            return contentType;
+    private IContentType GetContentType(string contentTypeAlias)
+    {
+        if (contentTypeAlias == null)
+        {
+            throw new ArgumentNullException(nameof(contentTypeAlias));
         }
 
-        private IContentType GetContentType(string contentTypeAlias)
+        if (string.IsNullOrWhiteSpace(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));
-            }
+            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);
-            }
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return GetContentType(scope, contentTypeAlias);
         }
+    }
 
-        #endregion
+    #endregion
 
-        #region Blueprints
+    #region Blueprints
 
-        public IContent? GetBlueprintById(int id)
+    public IContent? GetBlueprintById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IContent? blueprint = _documentBlueprintRepository.Get(id);
+            if (blueprint != null)
             {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IContent? blueprint = _documentBlueprintRepository.Get(id);
-                if (blueprint != null)
-                {
-                    blueprint.Blueprint = true;
-                }
-
-                return blueprint;
+                blueprint.Blueprint = true;
             }
+
+            return blueprint;
         }
+    }
 
-        public IContent? GetBlueprintById(Guid id)
+    public IContent? GetBlueprintById(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IContent? blueprint = _documentBlueprintRepository.Get(id);
+            if (blueprint != null)
             {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IContent? blueprint = _documentBlueprintRepository.Get(id);
-                if (blueprint != null)
-                {
-                    blueprint.Blueprint = true;
-                }
-
-                return blueprint;
+                blueprint.Blueprint = true;
             }
+
+            return blueprint;
         }
+    }
+
+    public void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
 
-        public void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
+        // always ensure the blueprint is at the root
+        if (content.ParentId != -1)
         {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
+            content.ParentId = -1;
+        }
 
-            //always ensure the blueprint is at the root
-            if (content.ParentId != -1)
-            {
-                content.ParentId = -1;
-            }
+        content.Blueprint = true;
 
-            content.Blueprint = true;
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            if (content.HasIdentity == false)
             {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                if (content.HasIdentity == false)
-                {
-                    content.CreatorId = userId;
-                }
+                content.CreatorId = userId;
+            }
 
-                content.WriterId = userId;
+            content.WriterId = userId;
 
-                _documentBlueprintRepository.Save(content);
+            _documentBlueprintRepository.Save(content);
 
-                Audit(AuditType.Save, Constants.Security.SuperUserId, content.Id,
-                    $"Saved content template: {content.Name}");
+            Audit(AuditType.Save, Constants.Security.SuperUserId, content.Id, $"Saved content template: {content.Name}");
 
-                scope.Notifications.Publish(new ContentSavedBlueprintNotification(content, evtMsgs));
+            scope.Notifications.Publish(new ContentSavedBlueprintNotification(content, evtMsgs));
 
-                scope.Complete();
-            }
+            scope.Complete();
         }
+    }
 
-        public void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
+    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();
-            }
+        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};
+    private static readonly string?[] ArrayOfOneNullString = { null };
 
-        public IContent CreateContentFromBlueprint(IContent blueprint, string name,
-            int userId = Constants.Security.SuperUserId)
+    public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId)
+    {
+        if (blueprint == null)
         {
-            if (blueprint == null)
-            {
-                throw new ArgumentNullException(nameof(blueprint));
-            }
+            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);
+        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;
+        content.CreatorId = userId;
+        content.WriterId = userId;
 
-            IEnumerable cultures = ArrayOfOneNullString;
-            if (blueprint.CultureInfos?.Count > 0)
+        IEnumerable cultures = ArrayOfOneNullString;
+        if (blueprint.CultureInfos?.Count > 0)
+        {
+            cultures = blueprint.CultureInfos.Values.Select(x => x.Culture);
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
-                cultures = blueprint.CultureInfos.Values.Select(x => x.Culture);
-                using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+                if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out ContentCultureInfos defaultCulture))
                 {
-                    if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(),
-                            out ContentCultureInfos defaultCulture))
-                    {
-                        defaultCulture.Name = name;
-                    }
-
-                    scope.Complete();
+                    defaultCulture.Name = name;
                 }
+
+                scope.Complete();
             }
+        }
 
-            DateTime now = DateTime.Now;
-            foreach (var culture in cultures)
+        DateTime now = DateTime.Now;
+        foreach (var culture in cultures)
+        {
+            foreach (IProperty property in blueprint.Properties)
             {
-                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);
-                }
+                var propertyCulture = property.PropertyType.VariesByCulture() ? culture : null;
+                content.SetValue(property.Alias, property.GetValue(propertyCulture), propertyCulture);
             }
 
-            return content;
+            if (!string.IsNullOrEmpty(culture))
+            {
+                content.SetCultureInfo(culture, blueprint.GetCultureName(culture), now);
+            }
         }
 
-        public IEnumerable GetBlueprintsForContentTypes(params int[] contentTypeId)
+        return content;
+    }
+
+    public IEnumerable GetBlueprintsForContentTypes(params int[] contentTypeId)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
+            IQuery query = Query();
+            if (contentTypeId.Length > 0)
             {
-                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;
-                });
+                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();
 
-        public void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds,
-            int userId = Constants.Security.SuperUserId)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
+            scope.WriteLock(Constants.Locks.ContentTree);
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            var contentTypeIdsA = contentTypeIds.ToArray();
+            IQuery query = Query();
+            if (contentTypeIdsA.Length > 0)
             {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var contentTypeIdsA = contentTypeIds.ToArray();
-                IQuery query = Query();
-                if (contentTypeIdsA.Length > 0)
-                {
-                    query.Where(x => contentTypeIdsA.Contains(x.ContentTypeId));
-                }
+                query.Where(x => contentTypeIdsA.Contains(x.ContentTypeId));
+            }
 
-                IContent[]? blueprints = _documentBlueprintRepository.Get(query)?.Select(x =>
-                {
-                    x.Blueprint = true;
-                    return x;
-                }).ToArray();
+            IContent[]? blueprints = _documentBlueprintRepository.Get(query)?.Select(x =>
+            {
+                x.Blueprint = true;
+                return x;
+            }).ToArray();
 
-                if (blueprints is not null)
+            if (blueprints is not null)
+            {
+                foreach (IContent blueprint in blueprints)
                 {
-                    foreach (IContent blueprint in blueprints)
-                    {
-                        _documentBlueprintRepository.Delete(blueprint);
-                    }
-
-                    scope.Notifications.Publish(new ContentDeletedBlueprintNotification(blueprints, evtMsgs));
-                    scope.Complete();
+                    _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);
+    public void DeleteBlueprintsOfType(int contentTypeId, int userId = Constants.Security.SuperUserId) =>
+        DeleteBlueprintsOfTypes(new[] { contentTypeId }, userId);
 
-        #endregion
-    }
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs
index 726c5b4435f8..b042612b1afc 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
+    #region RTE Anchor values
 
-        private static readonly Regex AnchorRegex = new Regex("", RegexOptions.Compiled);
+    private static readonly Regex AnchorRegex = new("", RegexOptions.Compiled);
 
-        public static IEnumerable GetAnchorValuesFromRTEs(this IContentService contentService, int id, string? culture = "*")
+    public static IEnumerable? GetByIds(this IContentService contentService, IEnumerable ids)
+    {
+        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)
-                {
-                    if (contentProperty.PropertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.TinyMce))
-                    {
-                        var value = contentProperty.GetValue(culture)?.ToString();
-                        if (!string.IsNullOrEmpty(value))
-                        {
-                            result.AddRange(contentService.GetAnchorValuesFromRTEContent(value));
-                        }
-                    }
-                }
+                throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) +
+                                                    " which is required by content");
             }
 
-            return result;
+            guids.Add(guidUdi);
         }
 
+        return contentService.GetByIds(guids.Select(x => x.Guid));
+    }
 
-        public static IEnumerable GetAnchorValuesFromRTEContent(this IContentService contentService, string rteContent)
+    /// 
+    ///     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)
         {
-            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;
+            throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) +
+                                                " which is required by content");
         }
-        #endregion
 
-        public static IEnumerable? GetByIds(this IContentService contentService, IEnumerable ids)
+        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)
         {
-            var guids = new List();
-            foreach (var udi in ids)
+            foreach (IProperty contentProperty in content.Properties)
             {
-                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);
+                if (contentProperty.PropertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases
+                        .TinyMce))
+                {
+                    var value = contentProperty.GetValue(culture)?.ToString();
+                    if (!string.IsNullOrEmpty(value))
+                    {
+                        result.AddRange(contentService.GetAnchorValuesFromRTEContent(value));
+                    }
+                }
             }
-
-            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);
-        }
+        return result;
+    }
 
-        /// 
-        /// Remove all permissions for this user for all nodes
-        /// 
-        /// 
-        /// 
-        public static void RemoveContentPermissions(this IContentService contentService, int contentId)
+    public static IEnumerable GetAnchorValuesFromRTEContent(
+        this IContentService contentService,
+        string rteContent)
+    {
+        var result = new List();
+        MatchCollection matches = AnchorRegex.Matches(rteContent);
+        foreach (Match match in matches)
         {
-            contentService.SetPermissions(new EntityPermissionSet(contentId, new EntityPermissionCollection()));
+            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 b493460876fe..36a790b9f689 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));
-            }
+            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)
+    // note: this should be a default interface method with C# 8
+    public IContentTypeComposition? GetContentTypeOf(IContentBase contentBase)
+    {
+        if (contentBase == null)
         {
-            if (contentBase == null) throw new ArgumentNullException(nameof(contentBase));
-            return For(contentBase).Get(contentBase.ContentTypeId);
+            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 8f7316d913a8..39adcf0daf72 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.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;
+            // 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();
         }
+    }
 
-        // 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);
+    /// 
+    ///     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);
+        }
+    }
 
-        protected override SavingNotification GetSavingNotification(IEnumerable items,
-            EventMessages eventMessages) => new ContentTypeSavingNotification(items, eventMessages);
+    /// 
+    ///     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 SavedNotification GetSavedNotification(IContentType item,
-            EventMessages eventMessages) => new ContentTypeSavedNotification(item, eventMessages);
+    protected override void DeleteItemsOfTypes(IEnumerable typeIds)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var typeIdsA = typeIds.ToArray();
+            ContentService.DeleteOfTypes(typeIdsA);
+            ContentService.DeleteBlueprintsOfTypes(typeIdsA);
+            scope.Complete();
+        }
+    }
 
-        protected override SavedNotification GetSavedNotification(IEnumerable items,
-            EventMessages eventMessages) => new ContentTypeSavedNotification(items, eventMessages);
+    #region Notifications
 
-        protected override DeletingNotification GetDeletingNotification(IContentType item,
-            EventMessages eventMessages) => new ContentTypeDeletingNotification(item, eventMessages);
+    protected override SavingNotification GetSavingNotification(
+        IContentType item,
+        EventMessages eventMessages) => new ContentTypeSavingNotification(item, eventMessages);
 
-        protected override DeletingNotification GetDeletingNotification(IEnumerable items,
-            EventMessages eventMessages) => new ContentTypeDeletingNotification(items, eventMessages);
+    protected override SavingNotification GetSavingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new ContentTypeSavingNotification(items, eventMessages);
 
-        protected override DeletedNotification GetDeletedNotification(IEnumerable items,
-            EventMessages eventMessages) => new ContentTypeDeletedNotification(items, eventMessages);
+    protected override SavedNotification GetSavedNotification(
+        IContentType item,
+        EventMessages eventMessages) => new ContentTypeSavedNotification(item, eventMessages);
 
-        protected override MovingNotification GetMovingNotification(MoveEventInfo moveInfo,
-            EventMessages eventMessages) => new ContentTypeMovingNotification(moveInfo, eventMessages);
+    protected override SavedNotification GetSavedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new ContentTypeSavedNotification(items, eventMessages);
 
-        protected override MovedNotification GetMovedNotification(
-            IEnumerable> moveInfo, EventMessages eventMessages) =>
-            new ContentTypeMovedNotification(moveInfo, eventMessages);
+    protected override DeletingNotification GetDeletingNotification(
+        IContentType item,
+        EventMessages eventMessages) => new ContentTypeDeletingNotification(item, eventMessages);
 
-        protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new ContentTypeChangedNotification(changes, eventMessages);
+    protected override DeletingNotification GetDeletingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new ContentTypeDeletingNotification(items, eventMessages);
 
-        protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new ContentTypeRefreshedNotification(changes, eventMessages);
+    protected override DeletedNotification GetDeletedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new ContentTypeDeletedNotification(items, eventMessages);
 
-        #endregion
+    protected override MovingNotification GetMovingNotification(
+        MoveEventInfo moveInfo,
+        EventMessages eventMessages) => new ContentTypeMovingNotification(moveInfo, eventMessages);
 
-        protected override void DeleteItemsOfTypes(IEnumerable typeIds)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                var typeIdsA = typeIds.ToArray();
-                ContentService.DeleteOfTypes(typeIdsA);
-                ContentService.DeleteBlueprintsOfTypes(typeIdsA);
-                scope.Complete();
-            }
-        }
+    protected override MovedNotification GetMovedNotification(
+        IEnumerable> moveInfo, EventMessages eventMessages) =>
+        new ContentTypeMovedNotification(moveInfo, eventMessages);
 
-        /// 
-        /// 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();
-            }
-        }
+    protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new ContentTypeChangedNotification(changes, eventMessages);
 
-        /// 
-        /// 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);
-            }
-        }
+    protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new ContentTypeRefreshedNotification(changes, eventMessages);
 
-        /// 
-        /// 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);
-            }
-        }
-    }
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs
index 1e97e02dca6a..7549cd849c65 100644
--- a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs
+++ b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs
@@ -2,12 +2,12 @@
 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 ce49a4f9d31c..98a7195fbfb4 100644
--- a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
+++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
@@ -5,1098 +5,1113 @@
 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;
-
-        protected ContentTypeServiceBase(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            TRepository repository, IAuditRepository auditRepository, IEntityContainerRepository containerRepository, IEntityRepository entityRepository,
-            IEventAggregator eventAggregator)
-            : base(provider, loggerFactory, eventMessagesFactory)
-        {
-            Repository = repository;
-            _auditRepository = auditRepository;
-            _containerRepository = containerRepository;
-            _entityRepository = entityRepository;
-            _eventAggregator = eventAggregator;
-        }
+        Repository = repository;
+        _auditRepository = auditRepository;
+        _containerRepository = containerRepository;
+        _entityRepository = entityRepository;
+        _eventAggregator = eventAggregator;
+    }
 
-        protected TRepository Repository { get; }
-        protected abstract int[] WriteLockIds { get; }
-        protected abstract int[] ReadLockIds { get; }
+    protected TRepository Repository { get; }
+    protected abstract int[] WriteLockIds { get; }
+    protected abstract int[] ReadLockIds { get; }
 
-        #region Notifications
+    #region Notifications
 
-        protected abstract SavingNotification GetSavingNotification(TItem item, EventMessages eventMessages);
-        protected abstract SavingNotification GetSavingNotification(IEnumerable items, EventMessages eventMessages);
+    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 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 DeletingNotification GetDeletingNotification(TItem item, EventMessages eventMessages);
+    protected abstract DeletingNotification GetDeletingNotification(IEnumerable items, EventMessages eventMessages);
 
-        protected abstract DeletedNotification GetDeletedNotification(IEnumerable items, EventMessages eventMessages);
+    protected abstract DeletedNotification GetDeletedNotification(IEnumerable items, EventMessages eventMessages);
 
-        protected abstract MovingNotification GetMovingNotification(MoveEventInfo moveInfo, EventMessages eventMessages);
+    protected abstract MovingNotification GetMovingNotification(MoveEventInfo moveInfo, EventMessages eventMessages);
 
-        protected abstract MovedNotification GetMovedNotification(IEnumerable> moveInfo, EventMessages eventMessages);
+    protected abstract MovedNotification GetMovedNotification(IEnumerable> moveInfo, EventMessages eventMessages);
 
-        protected abstract ContentTypeChangeNotification GetContentTypeChangedNotification(IEnumerable> changes, 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);
+    // 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
+    #endregion
 
-        #region Validation
+    #region Validation
 
-        public Attempt ValidateComposition(TItem? compo)
+    public Attempt ValidateComposition(TItem? compo)
+    {
+        try
         {
-            try
-            {
-                using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-                {
-                    scope.ReadLock(ReadLockIds);
-                    ValidateLocked(compo!);
-                }
-                return Attempt.Succeed();
-            }
-            catch (InvalidCompositionException ex)
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
             {
-                return Attempt.Fail(ex.PropertyTypeAliases, ex);
+                scope.ReadLock(ReadLockIds);
+                ValidateLocked(compo!);
             }
-        }
 
-        protected void ValidateLocked(TItem compositionContentType)
+            return Attempt.Succeed();
+        }
+        catch (InvalidCompositionException ex)
         {
-            // performs business-level validation of the composition
-            // should ensure that it is absolutely safe to save the composition
+            return Attempt.Fail(ex.PropertyTypeAliases, ex);
+        }
+    }
 
-            // 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)
+    protected void ValidateLocked(TItem compositionContentType)
+    {
+        // performs business-level validation of the composition
+        // should ensure that it is absolutely safe to save the composition
 
-            var allContentTypes = Repository.GetMany(new int[0]).Cast().ToArray();
+        // 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 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);
+        IContentTypeComposition[] allContentTypes = Repository.GetMany(new int[0]).Cast().ToArray();
 
-            var stack = new Stack();
-            foreach (var indirectReference in indirectReferences)
-                stack.Push(indirectReference); // push indirect references to a stack, so we can add recursively
+        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);
 
-            while (stack.Count > 0)
-            {
-                var indirectReference = stack.Pop();
-                dependencies.Add(indirectReference);
+        var stack = new Stack();
+        foreach (IContentTypeComposition indirectReference in indirectReferences)
+        {
+            stack.Push(indirectReference); // push indirect references to a stack, so we can add recursively
+        }
+
+        while (stack.Count > 0)
+        {
+            IContentTypeComposition indirectReference = stack.Pop();
+            dependencies.Add(indirectReference);
 
-                // get all compositions for the current indirect reference
-                var directReferences = indirectReference.ContentTypeComposition;
-                foreach (var directReference in directReferences)
+            // get all compositions for the current indirect reference
+            IEnumerable directReferences = indirectReference.ContentTypeComposition;
+            foreach (IContentTypeComposition directReference in directReferences)
+            {
+                if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias))
                 {
-                    if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias))
-                        continue;
+                    continue;
+                }
 
-                    dependencies.Add(directReference);
+                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);
+                // 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);
                 }
-
-                // recursive lookup of indirect references
-                foreach (var c in allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == indirectReference.Id)))
-                    stack.Push(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;
+                stack.Push(c);
+            }
+        }
 
-                var contentTypeDependency = allContentTypes.FirstOrDefault(x => x.Alias.Equals(dependency.Alias, StringComparison.InvariantCultureIgnoreCase));
-                if (contentTypeDependency == null)
-                    continue;
+        var duplicatePropertyTypeAliases = new List();
+        var invalidPropertyGroupAliases = new List();
 
-                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));
+        foreach (IContentTypeComposition dependency in dependencies)
+        {
+            if (dependency.Id == compositionContentType.Id)
+            {
+                continue;
             }
 
-            if (duplicatePropertyTypeAliases.Count > 0 || invalidPropertyGroupAliases.Count > 0)
-
+            IContentTypeComposition? contentTypeDependency = allContentTypes.FirstOrDefault(x => x.Alias.Equals(dependency.Alias, StringComparison.InvariantCultureIgnoreCase));
+            if (contentTypeDependency == null)
             {
-                throw new InvalidCompositionException(compositionContentType.Alias, null, duplicatePropertyTypeAliases.Distinct().ToArray(), invalidPropertyGroupAliases.Distinct().ToArray());
+                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));
         }
 
-        #endregion
+        if (duplicatePropertyTypeAliases.Count > 0 || invalidPropertyGroupAliases.Count > 0)
 
-        #region Composition
+        {
+            throw new InvalidCompositionException(compositionContentType.Alias, null, duplicatePropertyTypeAliases.Distinct().ToArray(), invalidPropertyGroupAliases.Distinct().ToArray());
+        }
+    }
+
+    #endregion
+
+    #region Composition
 
-        internal IEnumerable> ComposeContentTypeChanges(params TItem[] contentTypes)
+    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)
         {
-            // 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)
+            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)
             {
-                var dirty = (IRememberBeingDirty)contentType;
+                AddChange(changes, contentType, ContentTypeChangeTypes.Create);
+                continue;
+            }
 
-                // skip new content types
+            // 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 =>
-                {
-                    // 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;
+            // removed properties?
+            var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved");
 
-                    // alias change?
-                    return propertyType.WasPropertyDirty("Alias");
-                });
+            // removed compositions?
+            var hasAnyCompositionBeenRemoved = dirty.WasPropertyDirty("HasCompositionTypeBeenRemoved");
 
-                // removed properties?
-                var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved");
+            // variation changed?
+            var hasContentTypeVariationChanged = dirty.WasPropertyDirty("Variations");
 
-                // removed compositions?
-                var hasAnyCompositionBeenRemoved = dirty.WasPropertyDirty("HasCompositionTypeBeenRemoved");
+            // property variation change?
+            var hasAnyPropertyVariationChanged = contentType.WasPropertyTypeVariationChanged();
 
-                // variation changed?
-                var hasContentTypeVariationChanged = dirty.WasPropertyDirty("Variations");
+            // main impact on properties?
+            var hasPropertyMainImpact = hasContentTypeVariationChanged || hasAnyPropertyVariationChanged
+                                                                       || hasAnyCompositionBeenRemoved || hasAnyPropertyBeenRemoved || hasAnyPropertyChangedAlias;
 
-                // 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 (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
+                if (hasPropertyMainImpact)
                 {
-                    // add that one, as an other change
-                    AddChange(changes, contentType, ContentTypeChangeTypes.RefreshOther);
+                    foreach (TItem c in GetComposedOf(contentType.Id))
+                    {
+                        AddChange(changes, c, ContentTypeChangeTypes.RefreshMain);
+                    }
                 }
             }
-
-            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)
+            else
             {
-                changes.Add(new ContentTypeChange(contentType, changeTypes));
-                return;
+                // add that one, as an other change
+                AddChange(changes, contentType, ContentTypeChangeTypes.RefreshOther);
             }
-            change.ChangeTypes |= changeTypes;
         }
 
-        #endregion
-
-        #region Get, Has, Is, Count
+        return changes;
+    }
 
-        IContentTypeComposition? IContentTypeBaseService.Get(int id)
+    // 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)
         {
-            return Get(id);
+            changes.Add(new ContentTypeChange(contentType, changeTypes));
+            return;
         }
 
-        public TItem? Get(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.Get(id);
-            }
-        }
+        change.ChangeTypes |= changeTypes;
+    }
 
-        public TItem? Get(string alias)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.Get(alias);
-            }
-        }
+    #endregion
 
-        public TItem? Get(Guid id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.Get(id);
-            }
-        }
+    #region Get, Has, Is, Count
 
-        public IEnumerable GetAll(params int[] ids)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.GetMany(ids);
-            }
-        }
+    IContentTypeComposition? IContentTypeBaseService.Get(int id)
+    {
+        return Get(id);
+    }
 
-        public IEnumerable GetAll(IEnumerable? ids)
-        {
-            if (ids is null)
-            {
-                return Enumerable.Empty();
-            }
+    public TItem? Get(int id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.Get(id);
+    }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+    public TItem? Get(string alias)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.Get(alias);
+    }
 
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.GetMany(ids.ToArray());
-            }
-        }
+    public TItem? Get(Guid id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.Get(id);
+    }
 
-        public IEnumerable GetChildren(int 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)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                var query = Query().Where(x => x.ParentId == id);
-                return Repository.Get(query);
-            }
+            return Enumerable.Empty();
         }
 
-        public IEnumerable GetChildren(Guid id)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+
         {
-            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);
-            }
+            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 bool HasChildren(int id)
+    public IEnumerable GetChildren(Guid id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        TItem? found = Get(id);
+        if (found == null)
         {
-            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;
-            }
+            return Enumerable.Empty();
         }
 
-        public bool HasChildren(Guid id)
+        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))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            scope.ReadLock(ReadLockIds);
+            TItem? found = Get(id);
+            if (found == null)
             {
-                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;
+                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)
+    /// 
+    /// 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)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            TItem? self = Repository.Get(id);
+            if (self is not null)
             {
-                // can use same repo for both content and media
-                return Repository.HasContainerInPath(contentPath);
+                descendants.Add(self);
             }
         }
 
-        public bool HasContainerInPath(params int[] ids)
+        var ids = new Stack();
+        ids.Push(id);
+
+        while (ids.Count > 0)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            var i = ids.Pop();
+            IQuery query = Query().Where(x => x.ParentId == i);
+            TItem[]? result = Repository.Get(query).ToArray();
+
+            if (result is not null)
             {
-                // can use same repo for both content and media
-                return Repository.HasContainerInPath(ids);
+                foreach (TItem c in result)
+                {
+                    descendants.Add(c);
+                    ids.Push(c.Id);
+                }
             }
         }
 
-        public IEnumerable GetDescendants(int id, bool andSelf)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
+        return descendants.ToArray();
+    }
 
-                var descendants = new List();
-                if (andSelf)
-                {
-                    var self = Repository.Get(id);
-                    if (self is not null)
-                    {
-                        descendants.Add(self);
-                    }
-                }
-                var ids = new Stack();
-                ids.Push(id);
+    public IEnumerable GetComposedOf(int id, IEnumerable all) =>
+        all.Where(x => x.ContentTypeComposition.Any(y => y.Id == id));
 
-                while (ids.Count > 0)
-                {
-                    var i = ids.Pop();
-                    var query = Query().Where(x => x.ParentId == i);
-                    var result = Repository.Get(query).ToArray();
+    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);
+    }
 
-                    if (result is not null)
-                    {
-                        foreach (var c in result)
-                        {
-                            descendants.Add(c);
-                            ids.Push(c.Id);
-                        }
-                    }
-                }
+    public int Count()
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.Count(Query());
+    }
 
-                return descendants.ToArray();
-            }
-        }
+    public bool HasContentNodes(int id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.HasContentNodes(id);
+    }
 
-        public IEnumerable GetComposedOf(int id, IEnumerable all)
-        {
-            return all.Where(x => x.ContentTypeComposition.Any(y => y.Id == id));
+    #endregion
 
-        }
+    #region Save
 
-        public IEnumerable GetComposedOf(int id)
+    public void Save(TItem? item, int userId = Constants.Security.SuperUserId)
+    {
+        if (item is null)
         {
-            // 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);
+            return;
         }
 
-        public int Count()
+        using ICoreScope scope = ScopeProvider.CreateCoreScope();
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        SavingNotification savingNotification = GetSavingNotification(item, eventMessages);
+        if (scope.Notifications.PublishCancelable(savingNotification))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.Count(Query());
-            }
+            scope.Complete();
+            return;
         }
 
-        public bool HasContentNodes(int id)
+        if (string.IsNullOrWhiteSpace(item.Name))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.HasContentNodes(id);
-            }
+            throw new ArgumentException("Cannot save item with empty name.");
         }
 
-        #endregion
-
-        #region Save
-
-        public void Save(TItem? item, int userId = Cms.Core.Constants.Security.SuperUserId)
+        if (item.Name != null && item.Name.Length > 255)
         {
-            if (item is null)
-            {
-                return;
-            }
+            throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+        }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        scope.WriteLock(WriteLockIds);
 
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                SavingNotification savingNotification = GetSavingNotification(item, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
+        // validate the DAG transform, within the lock
+        ValidateLocked(item); // throws if invalid
 
-                if (string.IsNullOrWhiteSpace(item.Name))
-                {
-                    throw new ArgumentException("Cannot save item with empty name.");
-                }
+        item.CreatorId = userId;
+        if (item.Description == string.Empty)
+        {
+            item.Description = null;
+        }
 
-                if (item.Name != null && item.Name.Length > 255)
-                {
-                    throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
-                }
+        Repository.Save(item); // also updates content/media/member items
 
-                scope.WriteLock(WriteLockIds);
+        // figure out impacted content types
+        ContentTypeChange[] changes = ComposeContentTypeChanges(item).ToArray();
 
-                // validate the DAG transform, within the lock
-                ValidateLocked(item); // throws if invalid
+        // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
+        _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
 
-                item.CreatorId = userId;
-                if (item.Description == string.Empty)
-                {
-                    item.Description = null;
-                }
+        scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
 
-                Repository.Save(item); // also updates content/media/member items
+        SavedNotification savedNotification = GetSavedNotification(item, eventMessages);
+        savedNotification.WithStateFrom(savingNotification);
+        scope.Notifications.Publish(savedNotification);
 
-                // 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));
+        Audit(AuditType.Save, userId, item.Id);
+        scope.Complete();
+    }
 
-                SavedNotification savedNotification = GetSavedNotification(item, eventMessages);
-                savedNotification.WithStateFrom(savingNotification);
-                scope.Notifications.Publish(savedNotification);
+    public void Save(IEnumerable items, int userId = Constants.Security.SuperUserId)
+    {
+        TItem[] itemsA = items.ToArray();
 
-                Audit(AuditType.Save, userId, item.Id);
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            SavingNotification savingNotification = GetSavingNotification(itemsA, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
                 scope.Complete();
+                return;
             }
-        }
 
-        public void Save(IEnumerable items, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            TItem[] itemsA = items.ToArray();
+            scope.WriteLock(WriteLockIds);
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            // all-or-nothing, validate them all first
+            foreach (TItem contentType in itemsA)
             {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                SavingNotification savingNotification = GetSavingNotification(itemsA, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
+                ValidateLocked(contentType); // throws if invalid
+            }
+            foreach (TItem contentType in itemsA)
+            {
+                contentType.CreatorId = userId;
+                if (contentType.Description == string.Empty)
                 {
-                    scope.Complete();
-                    return;
+                    contentType.Description = null;
                 }
 
-                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);
+            }
 
-                    Repository.Save(contentType);
-                }
+            // figure out impacted content types
+            ContentTypeChange[] changes = ComposeContentTypeChanges(itemsA).ToArray();
 
-                // 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));
 
-                // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
-                _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages)); ;
+            scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
 
-                scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
+            SavedNotification savedNotification = GetSavedNotification(itemsA, eventMessages);
+            savedNotification.WithStateFrom(savingNotification);
+            scope.Notifications.Publish(savedNotification);
 
-                SavedNotification savedNotification = GetSavedNotification(itemsA, eventMessages);
-                savedNotification.WithStateFrom(savingNotification);
-                scope.Notifications.Publish(savedNotification);
-
-                Audit(AuditType.Save, userId, -1);
-                scope.Complete();
-            }
+            Audit(AuditType.Save, userId, -1);
+            scope.Complete();
         }
+    }
 
-        #endregion
+    #endregion
 
-        #region Delete
+    #region Delete
 
-        public void Delete(TItem item, int userId = Cms.Core.Constants.Security.SuperUserId)
+    public void Delete(TItem item, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            DeletingNotification deletingNotification = GetDeletingNotification(item, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                DeletingNotification deletingNotification = GetDeletingNotification(item, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                scope.WriteLock(WriteLockIds);
+                scope.Complete();
+                return;
+            }
 
-                // all descendants are going to be deleted
-                TItem[] descendantsAndSelf = GetDescendants(item.Id, true)
-                    .ToArray();
-                TItem[] deleted = descendantsAndSelf;
+            scope.WriteLock(WriteLockIds);
 
-                // 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();
+            // 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));
+            // 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));
-                }
+            // 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);
+            // 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();
+            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));
+            // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
+            _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
 
-                scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
+            scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
 
-                DeletedNotification deletedNotification = GetDeletedNotification(deleted.DistinctBy(x => x.Id), eventMessages);
-                deletedNotification.WithStateFrom(deletingNotification);
-                scope.Notifications.Publish(deletedNotification);
+            DeletedNotification deletedNotification = GetDeletedNotification(deleted.DistinctBy(x => x.Id), eventMessages);
+            deletedNotification.WithStateFrom(deletingNotification);
+            scope.Notifications.Publish(deletedNotification);
 
-                Audit(AuditType.Delete, userId, item.Id);
-                scope.Complete();
-            }
+            Audit(AuditType.Delete, userId, item.Id);
+            scope.Complete();
         }
+    }
 
-        public void Delete(IEnumerable items, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            TItem[] itemsA = items.ToArray();
+    public void Delete(IEnumerable items, int userId = Constants.Security.SuperUserId)
+    {
+        TItem[] itemsA = items.ToArray();
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            DeletingNotification deletingNotification = GetDeletingNotification(itemsA, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                DeletingNotification deletingNotification = GetDeletingNotification(itemsA, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
+                scope.Complete();
+                return;
+            }
 
-                scope.WriteLock(WriteLockIds);
+            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 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();
+            // 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));
+            // 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);
-                }
+            // 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();
+            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));
+            // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
+            _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
 
-                scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
+            scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
 
-                DeletedNotification deletedNotification = GetDeletedNotification(deleted.DistinctBy(x => x.Id), eventMessages);
-                deletedNotification.WithStateFrom(deletingNotification);
-                scope.Notifications.Publish(deletedNotification);
+            DeletedNotification deletedNotification = GetDeletedNotification(deleted.DistinctBy(x => x.Id), eventMessages);
+            deletedNotification.WithStateFrom(deletingNotification);
+            scope.Notifications.Publish(deletedNotification);
 
-                Audit(AuditType.Delete, userId, -1);
-                scope.Complete();
-            }
+            Audit(AuditType.Delete, userId, -1);
+            scope.Complete();
         }
+    }
 
-        protected abstract void DeleteItemsOfTypes(IEnumerable typeIds);
+    protected abstract void DeleteItemsOfTypes(IEnumerable typeIds);
 
-        #endregion
+    #endregion
 
-        #region Copy
+    #region Copy
 
-        public TItem Copy(TItem original, string alias, string name, int parentId = -1)
+    public TItem Copy(TItem original, string alias, string name, int parentId = -1)
+    {
+        TItem? parent = null;
+        if (parentId > 0)
         {
-            TItem? parent = null;
-            if (parentId > 0)
+            parent = Get(parentId);
+            if (parent == null)
             {
-                parent = Get(parentId);
-                if (parent == null)
-                {
-                    throw new InvalidOperationException("Could not find parent with id " + parentId);
-                }
+                throw new InvalidOperationException("Could not find parent with id " + parentId);
             }
-            return Copy(original, alias, name, parent);
         }
+        return Copy(original, alias, name, parent);
+    }
 
-        public TItem Copy(TItem original, string alias, string name, TItem? parent)
+    public TItem Copy(TItem original, string alias, string name, TItem? parent)
+    {
+        if (original == null)
         {
-            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);
-            }
+            throw new ArgumentNullException(nameof(original));
+        }
 
-            //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;
-            }
+        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));
+        }
 
-            Save(clone);
-            return clone;
+        if (parent != null && parent.HasIdentity == false)
+        {
+            throw new InvalidOperationException("Parent must have an identity.");
         }
 
-        public Attempt?> Copy(TItem copying, int containerId)
+        // 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)
         {
-            var eventMessages = EventMessagesFactory.Get();
+            clone.RemoveContentType(a);
+        }
 
-            TItem copy;
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(WriteLockIds);
+        //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();
 
-                try
+        TItem copy;
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(WriteLockIds);
+
+            try
+            {
+                if (containerId > 0)
                 {
-                    if (containerId > 0)
+                    EntityContainer? container = _containerRepository?.Get(containerId);
+                    if (container == null)
                     {
-                        var container = _containerRepository?.Get(containerId);
-                        if (container == null)
-                            throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback
+                        throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback
                     }
-                    var alias = Repository.GetUniqueAlias(copying.Alias);
+                }
+
+                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);
+                // 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
+                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)
+                // 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)
                     {
-                        var parent = Repository.Get(copy.ParentId);
-                        if (parent != null)
-                            copy.RemoveContentType(parent.Alias);
+                        copy.RemoveContentType(parent.Alias);
                     }
+                }
 
-                    copy.ParentId = containerId;
+                copy.ParentId = containerId;
 
-                    SavingNotification savingNotification = GetSavingNotification(copy, eventMessages);
-                    if (scope.Notifications.PublishCancelable(savingNotification))
-                    {
-                        scope.Complete();
-                        return OperationResult.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, eventMessages, copy);
-                    }
+                SavingNotification savingNotification = GetSavingNotification(copy, eventMessages);
+                if (scope.Notifications.PublishCancelable(savingNotification))
+                {
+                    scope.Complete();
+                    return OperationResult.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, eventMessages, copy);
+                }
 
-                    Repository.Save(copy);
+                Repository.Save(copy);
 
-                    ContentTypeChange[] changes = ComposeContentTypeChanges(copy).ToArray();
+                ContentTypeChange[] changes = ComposeContentTypeChanges(copy).ToArray();
 
-                    _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
-                    scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
+                _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
+                scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
 
-                    SavedNotification savedNotification = GetSavedNotification(copy, eventMessages);
-                    savedNotification.WithStateFrom(savingNotification);
-                    scope.Notifications.Publish(savedNotification);
+                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
-                }
+                scope.Complete();
+            }
+            catch (DataOperationException ex)
+            {
+                return OperationResult.Attempt.Fail(ex.Operation, eventMessages); // causes rollback
             }
-
-            return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, eventMessages, copy);
         }
 
-        #endregion
+        return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, eventMessages, copy);
+    }
 
-        #region Move
+    #endregion
 
-        public Attempt?> Move(TItem moving, int containerId)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
+    #region Move
 
-            var moveInfo = new List>();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+    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))
             {
-                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.Complete();
+                return OperationResult.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, eventMessages);
+            }
 
-                scope.WriteLock(WriteLockIds); // also for containers
+            scope.WriteLock(WriteLockIds); // also for containers
 
-                try
+            try
+            {
+                EntityContainer? container = null;
+                if (containerId > 0)
                 {
-                    EntityContainer? container = null;
-                    if (containerId > 0)
+                    container = _containerRepository?.Get(containerId);
+                    if (container == null)
                     {
-                        container = _containerRepository?.Get(containerId);
-                        if (container == null)
-                            throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback
+                        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);
+                moveInfo.AddRange(Repository.Move(moving, container!));
+                scope.Complete();
+            }
+            catch (DataOperationException ex)
+            {
+                scope.Complete();
+                return OperationResult.Attempt.Fail(ex.Operation, eventMessages);
             }
 
-            return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, 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);
         }
 
-        #endregion
+        return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, eventMessages);
+    }
+
+    #endregion
+
+    #region Containers
 
-        #region Containers
+    protected abstract Guid ContainedObjectType { get; }
 
-        protected abstract Guid ContainedObjectType { get; }
+    protected Guid ContainerObjectType => EntityContainer.GetContainerObjectType(ContainedObjectType);
 
-        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
 
-        public Attempt?> CreateContainer(int parentId, Guid key, string name, int userId = Cms.Core.Constants.Security.SuperUserId)
+        try
         {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            var container = new EntityContainer(ContainedObjectType)
             {
-                scope.WriteLock(WriteLockIds); // also for containers
+                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);
+            }
 
-                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();
 
-                    _containerRepository?.Save(container);
-                    scope.Complete();
+            var savedNotification = new EntityContainerSavedNotification(container, eventMessages);
+            savedNotification.WithStateFrom(savingNotification);
+            scope.Notifications.Publish(savedNotification);
+            // TODO: Audit trail ?
 
-                    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);
+        }
+    }
 
-                    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);
         }
 
-        public Attempt SaveContainer(EntityContainer container, int userId = Cms.Core.Constants.Security.SuperUserId)
+        if (container.HasIdentity && container.IsPropertyDirty("ParentId"))
         {
-            EventMessages eventMessages = EventMessagesFactory.Get();
+            var ex = new InvalidOperationException("Cannot save a container with a modified parent, move the container instead.");
+            return OperationResult.Attempt.Fail(eventMessages, ex);
+        }
 
-            Guid containerObjectType = ContainerObjectType;
-            if (container.ContainerObjectType != containerObjectType)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var savingNotification = new EntityContainerSavingNotification(container, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                var ex = new InvalidOperationException("Not a container of the proper type.");
-                return OperationResult.Attempt.Fail(eventMessages, ex);
+                scope.Complete();
+                return OperationResult.Attempt.Cancel(eventMessages);
             }
 
-            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);
-            }
+            scope.WriteLock(WriteLockIds); // also for containers
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var savingNotification = new EntityContainerSavingNotification(container, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Attempt.Cancel(eventMessages);
-                }
+            _containerRepository?.Save(container);
+            scope.Complete();
 
-                scope.WriteLock(WriteLockIds); // also for containers
+            var savedNotification = new EntityContainerSavedNotification(container, eventMessages);
+            savedNotification.WithStateFrom(savingNotification);
+            scope.Notifications.Publish(savedNotification);
+        }
 
-                _containerRepository?.Save(container);
-                scope.Complete();
+        // TODO: Audit trail ?
 
-                var savedNotification = new EntityContainerSavedNotification(container, eventMessages);
-                savedNotification.WithStateFrom(savingNotification);
-                scope.Notifications.Publish(savedNotification);
-            }
+        return OperationResult.Attempt.Succeed(eventMessages);
+    }
 
-            // TODO: Audit trail ?
+    public EntityContainer? GetContainer(int containerId)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds); // also for containers
 
-            return OperationResult.Attempt.Succeed(eventMessages);
-        }
+        return _containerRepository.Get(containerId);
+    }
 
-        public EntityContainer? GetContainer(int containerId)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds); // also for containers
+    public EntityContainer? GetContainer(Guid containerId)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds); // also for containers
 
-                return _containerRepository.Get(containerId);
-            }
-        }
+        return _containerRepository.Get(containerId);
+    }
 
-        public EntityContainer? GetContainer(Guid containerId)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds); // also for containers
+    public IEnumerable GetContainers(int[] containerIds)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds); // also for containers
 
-                return _containerRepository.Get(containerId);
-            }
-        }
+        return _containerRepository.GetMany(containerIds);
+    }
 
-        public IEnumerable GetContainers(int[] containerIds)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds); // also for containers
+    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 _containerRepository.GetMany(containerIds);
-            }
-        }
+        return GetContainers(ancestorIds);
+    }
 
-        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();
+    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);
+    }
 
-            return GetContainers(ancestorIds);
+    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);
         }
 
-        public IEnumerable GetContainers(string name, int level)
+        // '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)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds); // also for containers
+            scope.Complete();
+            return Attempt.Fail(new OperationResult(OperationResultType.FailedCannot, eventMessages));
+        }
 
-                return _containerRepository.Get(name, level);
-            }
+        var deletingNotification = new EntityContainerDeletingNotification(container, eventMessages);
+        if (scope.Notifications.PublishCancelable(deletingNotification))
+        {
+            scope.Complete();
+            return Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages));
         }
 
-        public Attempt DeleteContainer(int containerId, int userId = Cms.Core.Constants.Security.SuperUserId)
+        _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())
         {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            scope.WriteLock(WriteLockIds); // also for containers
+
+            try
             {
-                scope.WriteLock(WriteLockIds); // also for containers
+                EntityContainer? container = _containerRepository?.Get(id);
 
-                EntityContainer? container = _containerRepository?.Get(containerId);
+                //throw if null, this will be caught by the catch and a failed returned
                 if (container == null)
                 {
-                    return OperationResult.Attempt.NoOperation(eventMessages);
+                    throw new InvalidOperationException("No container found with id " + id);
                 }
 
-                // '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));
-                }
+                container.Name = name;
 
-                var deletingNotification = new EntityContainerDeletingNotification(container, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
+                var renamingNotification = new EntityContainerRenamingNotification(container, eventMessages);
+                if (scope.Notifications.PublishCancelable(renamingNotification))
                 {
                     scope.Complete();
-                    return Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages));
+                    return OperationResult.Attempt.Cancel(eventMessages);
                 }
 
-                _containerRepository?.Delete(container);
+                _containerRepository?.Save(container);
                 scope.Complete();
 
-                var deletedNotification = new EntityContainerDeletedNotification(container, eventMessages);
-                deletedNotification.WithStateFrom(deletingNotification);
-                scope.Notifications.Publish(deletedNotification);
+                var renamedNotification = new EntityContainerRenamedNotification(container, eventMessages);
+                renamedNotification.WithStateFrom(renamingNotification);
+                scope.Notifications.Publish(renamedNotification);
 
-                return OperationResult.Attempt.Succeed(eventMessages);
-                // TODO: Audit trail ?
+                return OperationResult.Attempt.Succeed(OperationResultType.Success, eventMessages, container);
             }
-        }
-
-        public Attempt?> RenameContainer(int id, string name, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            catch (Exception ex)
             {
-                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);
-                }
+                return OperationResult.Attempt.Fail(eventMessages, ex);
             }
         }
+    }
 
-        #endregion
+    #endregion
 
-        #region Audit
+    #region Audit
 
-        private void Audit(AuditType type, int userId, int objectId)
-        {
-            _auditRepository.Save(new AuditItem(objectId, type, userId,
-                ObjectTypes.GetUmbracoObjectType(ContainedObjectType).GetName()));
-        }
+    private void Audit(AuditType type, int userId, int objectId)
+    {
+        _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetUmbracoObjectType(ContainedObjectType).GetName()));
+    }
 
-        #endregion
+    #endregion
 
 
-    }
 }
diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs
index 6c30b24b67e3..5ae8da3a1236 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
+namespace Umbraco.Extensions;
+
+public static class ContentTypeServiceExtensions
 {
-    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)
     {
-        /// 
-        /// 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)
         {
-            if (contentTypeService == null)
-            {
-                return Enumerable.Empty();
-            }
+            return Enumerable.Empty();
+        }
 
-            return contentTypeService.GetAll().Where(x => x.IsElement);
+        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)
+        {
+            // if already in use a composition, do not allow any composited types
+            return new ContentTypeAvailableCompositionsResults();
         }
 
-        /// 
-        /// 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)
+        // 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));
+
+        // 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)
         {
-            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
-            var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray();
-            if (isUsing.Length > 0)
-            {
-                //if already in use a composition, do not allow any composited types
-                return new ContentTypeAvailableCompositionsResults();
-            }
+            list.Add(x);
+        }
+
+        // indirect types are those that we use, directly or indirectly
+        IContentTypeComposition[] indirectContentTypes = GetDirectOrIndirect(source).ToArray();
+        foreach (IContentTypeComposition x in indirectContentTypes)
+        {
+            list.Add(x);
+        }
+
+        // At this point we have a list of content types that 'could' be compositions
 
-            // 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));
-
-            // 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);
-
-            // 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)
+        // 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
+        IContentTypeComposition[] 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)
                 ? 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)
+    {
+        if (ctype == null)
+        {
+            return new IContentTypeComposition[] { };
+        }
 
-        private static IContentTypeComposition[] GetAncestors(IContentTypeComposition? ctype, IContentTypeComposition[] allContentTypes)
+        var ancestors = new List();
+        var parentId = ctype.ParentId;
+        while (parentId > 0)
         {
-            if (ctype == null) return new IContentTypeComposition[] {};
-            var ancestors = new List();
-            var parentId = ctype.ParentId;
-            while (parentId > 0)
+            IContentTypeComposition? parent = allContentTypes.FirstOrDefault(x => x.Id == parentId);
+            if (parent != null)
+            {
+                ancestors.Add(parent);
+                parentId = parent.ParentId;
+            }
+            else
             {
-                var parent = allContentTypes.FirstOrDefault(x => x.Id == parentId);
-                if (parent != null)
-                {
-                    ancestors.Add(parent);
-                    parentId = parent.ParentId;
-                }
-                else
-                {
-                    parentId = -1;
-                }
+                parentId = -1;
             }
-            return ancestors.ToArray();
         }
 
-        /// 
-        /// Get those that we use directly
-        /// 
-        /// 
-        /// 
-        private static IEnumerable GetDirectOrIndirect(IContentTypeComposition? ctype)
+        return ancestors.ToArray();
+    }
+
+    /// 
+    ///     Get those that we use directly
+    /// 
+    /// 
+    /// 
+    private static IEnumerable GetDirectOrIndirect(IContentTypeComposition? ctype)
+    {
+        if (ctype == null)
         {
-            if (ctype == null) return Enumerable.Empty();
+            return Enumerable.Empty();
+        }
 
-            // hashset guarantees uniqueness on Id
-            var all = new HashSet(new DelegateEqualityComparer(
-                (x, y) => x?.Id == y?.Id,
-                x => x.Id));
+        // hashset guarantees uniqueness on Id
+        var all = new HashSet(new DelegateEqualityComparer(
+            (x, y) => x?.Id == y?.Id,
+            x => x.Id));
 
-            var stack = new Stack();
+        var stack = new Stack();
 
-            foreach (var x in ctype.ContentTypeComposition)
-                stack.Push(x);
+        foreach (IContentTypeComposition x in ctype.ContentTypeComposition)
+        {
+            stack.Push(x);
+        }
 
-            while (stack.Count > 0)
+        while (stack.Count > 0)
+        {
+            IContentTypeComposition x = stack.Pop();
+            all.Add(x);
+            foreach (IContentTypeComposition y in x.ContentTypeComposition)
             {
-                var x = stack.Pop();
-                all.Add(x);
-                foreach (var y in x.ContentTypeComposition)
-                    stack.Push(y);
+                stack.Push(y);
             }
-
-            return all;
         }
+
+        return all;
     }
 }
diff --git a/src/Umbraco.Core/Services/ContentVersionService.cs b/src/Umbraco.Core/Services/ContentVersionService.cs
index 9e32bab7622d..24443a3957db 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.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;
-
-        public ContentVersionService(
-            ILogger logger,
-            IDocumentVersionRepository documentVersionRepository,
-            IContentVersionCleanupPolicy contentVersionCleanupPolicy,
-            ICoreScopeProvider scopeProvider,
-            IEventMessagesFactory eventMessagesFactory,
-            IAuditRepository auditRepository,
-            ILanguageRepository languageRepository)
+        _logger = logger;
+        _documentVersionRepository = documentVersionRepository;
+        _contentVersionCleanupPolicy = contentVersionCleanupPolicy;
+        _scopeProvider = scopeProvider;
+        _eventMessagesFactory = eventMessagesFactory;
+        _auditRepository = auditRepository;
+        _languageRepository = 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;
-
-            /* 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();
+            var languageId = _languageRepository.GetIdByIsoCode(culture, true);
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentVersionRepository.GetPagedItemsByContentId(contentId, pageIndex, pageSize, out totalRecords, languageId);
+        }
+    }
 
-                if (allHistoricVersions is null)
-                {
-                    return Array.Empty();
-                }
-                _logger.LogDebug("Discovered {count} candidate(s) for ContentVersion cleanup", allHistoricVersions.Count);
-                versionsToDelete = new List(allHistoricVersions.Count);
+    /// 
+    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);
 
-                IEnumerable filteredContentVersions = _contentVersionCleanupPolicy.Apply(asAtDate, allHistoricVersions);
+            ContentVersionMeta? version = _documentVersionRepository.Get(versionId);
 
-                foreach (ContentVersionMeta version in filteredContentVersions)
-                {
-                    EventMessages messages = _eventMessagesFactory.Get();
+            if (version is null)
+            {
+                return;
+            }
 
-                    if (scope.Notifications.PublishCancelable(new ContentDeletingVersionsNotification(version.ContentId, messages, version.VersionId)))
-                    {
-                        _logger.LogDebug("Delete cancelled for ContentVersion [{versionId}]", version.VersionId);
-                        continue;
-                    }
+            AuditType auditType = preventCleanup
+                ? AuditType.ContentVersionPreventCleanup
+                : AuditType.ContentVersionEnableCleanup;
 
-                    versionsToDelete.Add(version);
-                }
-            }
+            var message = $"set preventCleanup = '{preventCleanup}' for version '{versionId}'";
 
-            if (!versionsToDelete.Any())
+            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))
-            {
-                using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
-                {
-                    scope.WriteLock(Constants.Locks.ContentTree);
-                    var groupEnumerated = group.ToList();
-                    _documentVersionRepository.DeleteVersions(groupEnumerated.Select(x => x.VersionId));
+            IEnumerable filteredContentVersions =
+                _contentVersionCleanupPolicy.Apply(asAtDate, allHistoricVersions);
 
-                    foreach (ContentVersionMeta version in groupEnumerated)
-                    {
-                        EventMessages messages = _eventMessagesFactory.Get();
+            foreach (ContentVersionMeta version in filteredContentVersions)
+            {
+                EventMessages messages = _eventMessagesFactory.Get();
 
-                        scope.Notifications.Publish(new ContentDeletedVersionsNotification(version.ContentId, messages, version.VersionId));
-                    }
+                if (scope.Notifications.PublishCancelable(
+                        new ContentDeletingVersionsNotification(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);
-
-                ContentVersionMeta? version = _documentVersionRepository.Get(versionId);
+                var groupEnumerated = group.ToList();
+                _documentVersionRepository.DeleteVersions(groupEnumerated.Select(x => x.VersionId));
 
-                if (version is null)
+                foreach (ContentVersionMeta version in groupEnumerated)
                 {
-                    return;
-                }
-
-                AuditType auditType = preventCleanup
-                    ? AuditType.ContentVersionPreventCleanup
-                    : AuditType.ContentVersionEnableCleanup;
-
-                var message = $"set preventCleanup = '{preventCleanup}' for version '{versionId}'";
+                    EventMessages messages = _eventMessagesFactory.Get();
 
-                Audit(auditType, userId, version.ContentId, message, $"{version.VersionDate}");
+                    scope.Notifications.Publish(
+                        new ContentDeletedVersionsNotification(version.ContentId, messages, version.VersionId));
+                }
             }
         }
 
-        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 203ce64984d0..f5ddb30557bf 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
-    {
-        // TODO: Unit test all this!!! :/
+    private readonly DashboardCollection _dashboardCollection;
 
-        private readonly ISectionService _sectionService;
-        private readonly DashboardCollection _dashboardCollection;
-        private readonly ILocalizedTextService _localizedText;
+    private readonly ILocalizedTextService _localizedText;
 
-        public DashboardService(ISectionService sectionService, DashboardCollection dashboardCollection, ILocalizedTextService localizedText)
-        {
-            _sectionService = sectionService ?? throw new ArgumentNullException(nameof(sectionService));
-            _dashboardCollection = dashboardCollection ?? throw new ArgumentNullException(nameof(dashboardCollection));
-            _localizedText = localizedText ?? throw new ArgumentNullException(nameof(localizedText));
-        }
+    // TODO: Unit test all this!!! :/
+    private readonly ISectionService _sectionService;
+
+    public DashboardService(ISectionService sectionService, DashboardCollection dashboardCollection, ILocalizedTextService localizedText)
+    {
+        _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;
 
-        /// 
-        public IEnumerable> GetDashboards(string section, IUser? currentUser)
+        foreach (IDashboard dashboard in _dashboardCollection.Where(x => x.Sections.InvariantContains(section)))
         {
-            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.");
+                continue;
+            }
 
-                var dashboards = new List { dashboard };
-                tabs.Add(new Tab
-                {
-                    Id = tabId++,
-                    Label = _localizedText.Localize("dashboardTabs", dashboard.Alias),
-                    Alias = dashboard.Alias,
-                    Properties = dashboards
-                });
+            if (dashboard.View?.InvariantEndsWith(".ascx") ?? false)
+            {
+                throw new NotSupportedException("Legacy UserControl (.ascx) dashboards are no longer supported.");
             }
 
-            return tabs;
+            var dashboards = new List { dashboard };
+            tabs.Add(new Tab
+            {
+                Id = tabId++,
+                Label = _localizedText.Localize("dashboardTabs", dashboard.Alias),
+                Alias = dashboard.Alias,
+                Properties = dashboards,
+            });
         }
 
-        /// 
-        public IDictionary>> GetDashboards(IUser? currentUser)
+        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)
         {
-            return _sectionService.GetSections().ToDictionary(x => x.Alias, x => GetDashboards(x.Alias, currentUser));
+            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.");
+            }
         }
 
-        private bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IEnumerable rules)
+        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)
         {
-            if (user.Id == Constants.Security.SuperUserId)
-                return true;
+            return true;
+        }
+
+        (IAccessRule[] denyRules, IAccessRule[] grantRules, IAccessRule[] grantBySectionRules) = GroupRules(rules);
 
-            var (denyRules, grantRules, grantBySectionRules) = GroupRules(rules);
+        var hasAccess = true;
+        string[]? assignedUserGroups = null;
 
-            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;
 
-            // 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)
+            // 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)
             {
-                hasAccess = false;
+                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();
 
-                // 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)
+                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;
+                    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 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;
+                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;
-
-            // 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;
-
+        // 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)
-        {
-            IAccessRule[]? denyRules = null, grantRules = null, grantBySectionRules = null;
+        // 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();
 
-            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());
+        if (deniedUserGroups.Intersect(assignedUserGroups).Any())
+        {
+            hasAccess = false;
         }
+
+        return hasAccess;
     }
 }
diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs
index 5c9d5847edc6..1fdbb4a79b6e 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 @@ public class DataTypeService : RepositoryService, IDataTypeService
         [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 @@ public DataTypeService(
 
         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 @@ public DataTypeService(
 
         #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 @@ public DataTypeService(
 
         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 @@ public IEnumerable GetContainers(IDataType dataType)
             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 @@ public IEnumerable GetContainers(IDataType dataType)
 
         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 @@ public IEnumerable GetContainers(int[] containerIds)
                 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 @@ public IEnumerable GetContainers(int[] containerIds)
             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 @@ public IEnumerable GetContainers(int[] containerIds)
             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 @@ public IEnumerable GetContainers(int[] containerIds)
         /// 
         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 @@ public IEnumerable GetContainers(int[] containerIds)
         /// 
         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 @@ public IEnumerable GetContainers(int[] containerIds)
         /// 
         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 @@ public IEnumerable GetContainers(int[] containerIds)
         /// 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 @@ public IEnumerable GetByEditorAlias(string propertyEditorAlias)
         /// 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 @@ private void ConvertMissingEditorsOfDataTypesToLabels(IEnumerable dat
         {
             // 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 @@ private void ConvertMissingEditorsOfDataTypesToLabels(IEnumerable dat
 
         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 @@ private void ConvertMissingEditorsOfDataTypesToLabels(IEnumerable dat
                     {
                         container = _dataTypeContainerRepository.Get(parentId);
                         if (container == null)
+                        {
                             throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback
+                        }
                     }
                     moveInfo.AddRange(_dataTypeRepository.Move(toMove, container));
 
@@ -439,39 +441,37 @@ private void ConvertMissingEditorsOfDataTypesToLabels(IEnumerable dat
         /// 
         ///  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())
-            {
-                var saveEventArgs = new SaveEventArgs(dataType);
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var saveEventArgs = new SaveEventArgs(dataType);
 
-                var savingDataTypeNotification = new DataTypeSavingNotification(dataType, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingDataTypeNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
+            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 (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.");
-                }
+            if (dataType.Name != null && dataType.Name.Length > 255)
+            {
+                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+            }
 
-                _dataTypeRepository.Save(dataType);
+            _dataTypeRepository.Save(dataType);
 
-                scope.Notifications.Publish(new DataTypeSavedNotification(dataType, evtMsgs).WithStateFrom(savingDataTypeNotification));
+            scope.Notifications.Publish(new DataTypeSavedNotification(dataType, evtMsgs).WithStateFrom(savingDataTypeNotification));
 
-                Audit(AuditType.Save, userId, dataType.Id);
-                scope.Complete();
-            }
+            Audit(AuditType.Save, userId, dataType.Id);
+            scope.Complete();
         }
 
         /// 
@@ -481,30 +481,28 @@ public void Save(IDataType dataType, int userId = Cms.Core.Constants.Security.Su
         /// 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;
-                }
+                scope.Complete();
+                return;
+            }
 
-                foreach (var dataTypeDefinition in dataTypeDefinitionsA)
-                {
-                    dataTypeDefinition.CreatorId = userId;
-                    _dataTypeRepository.Save(dataTypeDefinition);
-                }
+            foreach (IDataType dataTypeDefinition in dataTypeDefinitionsA)
+            {
+                dataTypeDefinition.CreatorId = userId;
+                _dataTypeRepository.Save(dataTypeDefinition);
+            }
 
-                scope.Notifications.Publish(new DataTypeSavedNotification(dataTypeDefinitions, evtMsgs).WithStateFrom(savingDataTypeNotification));
+            scope.Notifications.Publish(new DataTypeSavedNotification(dataTypeDefinitions, evtMsgs).WithStateFrom(savingDataTypeNotification));
 
-                Audit(AuditType.Save, userId, -1);
+            Audit(AuditType.Save, userId, -1);
 
-                scope.Complete();
-            }
+            scope.Complete();
         }
 
         /// 
@@ -516,64 +514,60 @@ public void Save(IEnumerable dataTypeDefinitions, int userId)
         /// 
         ///  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.
+                // 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);
-                }
+                _contentTypeRepository.Save(contentType);
+            }
 
-                _dataTypeRepository.Delete(dataType);
+            _dataTypeRepository.Delete(dataType);
 
-                scope.Notifications.Publish(new DataTypeDeletedNotification(dataType, evtMsgs).WithStateFrom(deletingDataTypeNotification));
+            scope.Notifications.Publish(new DataTypeDeletedNotification(dataType, evtMsgs).WithStateFrom(deletingDataTypeNotification));
 
-                Audit(AuditType.Delete, userId, dataType.Id);
+            Audit(AuditType.Delete, userId, dataType.Id);
 
-                scope.Complete();
-            }
+            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 312b939ec5fd..476a2ddd4752 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);
+            return false; // built in ones can never be ignoring start nodes
+        }
 
-            if (dataType != null && dataType.Configuration is IIgnoreUserStartNodesConfig ignoreStartNodesConfig)
-                return ignoreStartNodesConfig.IgnoreUserStartNodes;
+        IDataType? dataType = dataTypeService.GetDataType(key);
 
-            return false;
+        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 810106e0ba3d..f51858fa5b25 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.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;
-
-        public DefaultContentVersionCleanupPolicy(IOptions contentSettings, ICoreScopeProvider scopeProvider, IDocumentVersionRepository documentVersionRepository)
-        {
-            _contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings));
-            _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
-            _documentVersionRepository = documentVersionRepository ?? throw new ArgumentNullException(nameof(documentVersionRepository));
-        }
+        _contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings));
+        _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
+        _documentVersionRepository = documentVersionRepository ??
+                                     throw new ArgumentNullException(nameof(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.
+    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 globalPolicy = _contentSettings.Value.ContentVersionCleanupPolicy;
+        var theRest = new List();
 
-            var theRest = new List();
+        using (_scopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            var policyOverrides = _documentVersionRepository.GetCleanupPolicies()?
+                .ToDictionary(x => x.ContentTypeId);
 
-            using(_scopeProvider.CreateCoreScope(autoComplete: true))
+            foreach (ContentVersionMeta version in items)
             {
-                var policyOverrides = _documentVersionRepository.GetCleanupPolicies()?
-                    .ToDictionary(x => x.ContentTypeId);
-
-                foreach (var version in items)
-                {
-                    var age = asAtDate - version.VersionDate;
-
-                    var overrides = GetOverridePolicy(version, policyOverrides);
+                TimeSpan age = asAtDate - version.VersionDate;
 
-                    var keepAll = overrides?.KeepAllVersionsNewerThanDays ?? globalPolicy.KeepAllVersionsNewerThanDays!;
-                    var keepLatest = overrides?.KeepLatestVersionPerDayForDays ?? globalPolicy.KeepLatestVersionPerDayForDays;
-                    var preventCleanup = overrides?.PreventCleanup ?? false;
+                ContentVersionCleanupPolicySettings? overrides = GetOverridePolicy(version, policyOverrides);
 
-                    if (preventCleanup)
-                    {
-                        continue;
-                    }
+                var keepAll = overrides?.KeepAllVersionsNewerThanDays ?? globalPolicy.KeepAllVersionsNewerThanDays;
+                var keepLatest = overrides?.KeepLatestVersionPerDayForDays ??
+                                 globalPolicy.KeepLatestVersionPerDayForDays;
+                var preventCleanup = overrides?.PreventCleanup ?? false;
 
-                    if (age.TotalDays <= keepAll)
-                    {
-                        continue;
-                    }
-
-                    if (age.TotalDays > keepLatest)
-                    {
-
-                        yield return version;
-                        continue;
-                    }
+                if (preventCleanup)
+                {
+                    continue;
+                }
 
-                    theRest.Add(version);
+                if (age.TotalDays <= keepAll)
+                {
+                    continue;
                 }
 
-                var grouped = theRest.GroupBy(x => new
+                if (age.TotalDays > keepLatest)
                 {
-                    x.ContentId,
-                    x.VersionDate.Date
-                });
+                    yield return version;
+                    continue;
+                }
 
-                foreach (var group in grouped)
+                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))
                 {
-                    foreach (var version in group.OrderByDescending(x => x.VersionId).Skip(1))
-                    {
-                        yield return version;
-                    }
+                    yield return version;
                 }
             }
         }
+    }
 
-        private ContentVersionCleanupPolicySettings? GetOverridePolicy(
-            ContentVersionMeta version,
-            IDictionary? overrides)
+    private ContentVersionCleanupPolicySettings? GetOverridePolicy(
+        ContentVersionMeta version,
+        IDictionary? overrides)
+    {
+        if (overrides is null)
         {
-            if (overrides is null)
-            {
-                return null;
-            }
+            return null;
+        }
 
-            _ = overrides.TryGetValue(version.ContentTypeId, out var value);
+        _ = overrides.TryGetValue(version.ContentTypeId, out ContentVersionCleanupPolicySettings? value);
 
-            return value;
-        }
+        return value;
     }
 }
diff --git a/src/Umbraco.Core/Services/DomainService.cs b/src/Umbraco.Core/Services/DomainService.cs
index b319f0fc42a6..38f27bb94c37 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.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;
+    private readonly IDomainRepository _domainRepository;
 
-        public DomainService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IDomainRepository domainRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
-        {
-            _domainRepository = domainRepository;
-        }
+    public DomainService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IDomainRepository domainRepository)
+        : base(provider, loggerFactory, eventMessagesFactory) =>
+        _domainRepository = domainRepository;
 
-        public bool Exists(string domainName)
+    public bool Exists(string domainName)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.Exists(domainName);
-            }
+            return _domainRepository.Exists(domainName);
         }
+    }
 
-        public Attempt Delete(IDomain domain)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
+    public Attempt Delete(IDomain domain)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var deletingNotification = new DomainDeletingNotification(domain, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                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.Cancel(eventMessages);
             }
 
-            return OperationResult.Attempt.Succeed(eventMessages);
+            _domainRepository.Delete(domain);
+            scope.Complete();
+
+            scope.Notifications.Publish(
+                new DomainDeletedNotification(domain, eventMessages).WithStateFrom(deletingNotification));
         }
 
-        public IDomain? GetByName(string name)
+        return OperationResult.Attempt.Succeed(eventMessages);
+    }
+
+    public IDomain? GetByName(string name)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.GetByName(name);
-            }
+            return _domainRepository.GetByName(name);
         }
+    }
 
-        public IDomain? GetById(int id)
+    public IDomain? GetById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.Get(id);
-            }
+            return _domainRepository.Get(id);
         }
+    }
 
-        public IEnumerable GetAll(bool includeWildcards)
+    public IEnumerable GetAll(bool includeWildcards)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.GetAll(includeWildcards);
-            }
+            return _domainRepository.GetAll(includeWildcards);
         }
+    }
 
-        public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards)
+    public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.GetAssignedDomains(contentId, includeWildcards);
-            }
+            return _domainRepository.GetAssignedDomains(contentId, includeWildcards);
         }
+    }
 
-        public Attempt Save(IDomain domainEntity)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
+    public Attempt Save(IDomain domainEntity)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var savingNotification = new DomainSavingNotification(domainEntity, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                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.Cancel(eventMessages);
             }
 
-            return OperationResult.Attempt.Succeed(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 5fa7ed24f739..591fa17909fe 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.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;
-
-        public EntityService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IIdKeyMap idKeyMap, IEntityRepository entityRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
-        {
-            _idKeyMap = idKeyMap;
-            _entityRepository = entityRepository;
-
-            _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 },
-            };
-        }
+        _idKeyMap = idKeyMap;
+        _entityRepository = entityRepository;
+
+        _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
-            ?? (_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 ??= Query()
+                                                          .Where(x => x.ParentId == -1);
 
-        #endregion
+    #endregion
 
-        // gets the object type, throws if not supported
-        private UmbracoObjectTypes GetObjectType(Type ?type)
-        {
-            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)
+    /// 
+    public IEntitySlim? Get(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(id);
-            }
+            return _entityRepository.Get(id);
         }
+    }
 
-        /// 
-        public IEntitySlim? Get(Guid key)
+    /// 
+    public IEntitySlim? Get(Guid key)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(key);
-            }
+            return _entityRepository.Get(key);
         }
+    }
 
-        /// 
-        public virtual IEntitySlim? Get(int id, UmbracoObjectTypes objectType)
+    /// 
+    public virtual IEntitySlim? Get(int id, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(id, objectType.GetGuid());
-            }
+            return _entityRepository.Get(id, objectType.GetGuid());
         }
+    }
 
-        /// 
-        public IEntitySlim? Get(Guid key, UmbracoObjectTypes objectType)
+    /// 
+    public IEntitySlim? Get(Guid key, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(key, objectType.GetGuid());
-            }
+            return _entityRepository.Get(key, objectType.GetGuid());
         }
+    }
 
-        /// 
-        public virtual IEntitySlim? Get(int id)
-            where T : IUmbracoEntity
+    /// 
+    public virtual IEntitySlim? Get(int id)
+        where T : IUmbracoEntity
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(id);
-            }
+            return _entityRepository.Get(id);
         }
+    }
 
-        /// 
-        public virtual IEntitySlim? Get(Guid key)
-            where T : IUmbracoEntity
+    /// 
+    public virtual IEntitySlim? Get(Guid key)
+        where T : IUmbracoEntity
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(key);
-            }
+            return _entityRepository.Get(key);
         }
+    }
 
-        /// 
-        public bool Exists(int id)
+    /// 
+    public bool Exists(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Exists(id);
-            }
+            return _entityRepository.Exists(id);
         }
+    }
 
-        /// 
-        public bool Exists(Guid key)
+    /// 
+    public bool Exists(Guid key)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Exists(key);
-            }
+            return _entityRepository.Exists(key);
         }
+    }
 
+    /// 
+    public virtual IEnumerable GetAll()
+        where T : IUmbracoEntity
+        => GetAll(Array.Empty());
 
-        /// 
-        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();
 
-        /// 
-        public virtual IEnumerable GetAll(params int[] ids)
-            where T : IUmbracoEntity
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            var entityType = typeof (T);
-            var objectType = GetObjectType(entityType);
-            var objectTypeId = objectType.GetGuid();
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectTypeId, ids);
-            }
+            return _entityRepository.GetAll(objectTypeId, ids);
         }
+    }
 
-        /// 
-        public virtual IEnumerable GetAll(UmbracoObjectTypes objectType)
-            => GetAll(objectType, Array.Empty());
+    /// 
+    public virtual IEnumerable GetAll(UmbracoObjectTypes objectType)
+        => GetAll(objectType, Array.Empty());
 
-        /// 
-        public virtual IEnumerable GetAll(UmbracoObjectTypes objectType, params int[] ids)
+    /// 
+    public virtual IEnumerable GetAll(UmbracoObjectTypes objectType, params int[] ids)
+    {
+        Type? entityType = objectType.GetClrType();
+        if (entityType == null)
         {
-            var entityType = objectType.GetClrType();
-            if (entityType == null)
-                throw new NotSupportedException($"Type \"{objectType}\" is not supported here.");
+            throw new NotSupportedException($"Type \"{objectType}\" is not supported here.");
+        }
 
-            GetObjectType(entityType);
+        GetObjectType(entityType);
 
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectType.GetGuid(), ids);
-            }
+        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)
+        => GetAll(objectType, Array.Empty());
 
-        /// 
-        public virtual IEnumerable GetAll(Guid objectType, params int[] ids)
-        {
-            var entityType = ObjectTypes.GetClrType(objectType);
-            GetObjectType(entityType);
+    /// 
+    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);
-            }
+        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();
+    /// 
+    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);
-            }
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAll(objectTypeId, keys);
         }
+    }
 
-        /// 
-        public IEnumerable GetAll(UmbracoObjectTypes objectType, Guid[] keys)
-        {
-            var entityType = objectType.GetClrType();
-            GetObjectType(entityType);
+    /// 
+    public IEnumerable GetAll(UmbracoObjectTypes objectType, Guid[] keys)
+    {
+        Type? entityType = objectType.GetClrType();
+        GetObjectType(entityType);
 
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectType.GetGuid(), keys);
-            }
+        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);
+    /// 
+    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);
-            }
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAll(objectType, keys);
         }
+    }
 
-        /// 
-        public virtual IEnumerable GetRootEntities(UmbracoObjectTypes objectType)
+    /// 
+    public virtual IEnumerable GetRootEntities(UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetByQuery(QueryRootEntity, objectType.GetGuid());
-            }
+            return _entityRepository.GetByQuery(QueryRootEntity, objectType.GetGuid());
         }
+    }
 
-        /// 
-        public virtual IEntitySlim? GetParent(int id)
+    /// 
+    public virtual IEntitySlim? GetParent(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
+            IEntitySlim? entity = _entityRepository.Get(id);
+            if (entity is null || entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21)
             {
-                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);
+                return null;
             }
+
+            return _entityRepository.Get(entity.ParentId);
         }
+    }
 
-        /// 
-        public virtual IEntitySlim? GetParent(int id, UmbracoObjectTypes objectType)
+    /// 
+    public virtual IEntitySlim? GetParent(int id, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
+            IEntitySlim? entity = _entityRepository.Get(id);
+            if (entity is null || entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21)
             {
-                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());
+                return null;
             }
+
+            return _entityRepository.Get(entity.ParentId, objectType.GetGuid());
         }
+    }
 
-        /// 
-        public virtual IEnumerable GetChildren(int parentId)
+    /// 
+    public virtual IEnumerable GetChildren(int parentId)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.ParentId == parentId);
-                return _entityRepository.GetByQuery(query);
-            }
+            IQuery query = Query().Where(x => x.ParentId == parentId);
+            return _entityRepository.GetByQuery(query);
         }
+    }
 
-        /// 
-        public virtual IEnumerable GetChildren(int parentId, UmbracoObjectTypes objectType)
+    /// 
+    public virtual IEnumerable GetChildren(int parentId, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.ParentId == parentId);
-                return _entityRepository.GetByQuery(query, objectType.GetGuid());
-            }
+            IQuery query = Query().Where(x => x.ParentId == parentId);
+            return _entityRepository.GetByQuery(query, objectType.GetGuid());
         }
+    }
 
-        /// 
-        public virtual IEnumerable GetDescendants(int id)
+    /// 
+    public virtual IEnumerable GetDescendants(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            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);
-            }
+            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)
+    /// 
+    public virtual IEnumerable GetDescendants(int id, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
+            IEntitySlim? entity = _entityRepository.Get(id);
+            if (entity is null)
             {
-                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());
+                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)
+    /// 
+    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))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.ParentId == id && x.Trashed == false);
+            IQuery query = Query().Where(x => x.ParentId == id && x.Trashed == false);
 
-                return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
+            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)
+    /// 
+    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))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var objectTypeGuid = objectType.GetGuid();
-                var query = Query();
+            Guid objectTypeGuid = objectType.GetGuid();
+            IQuery query = Query();
 
-                if (id != Cms.Core.Constants.System.Root)
+            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)
                 {
-                    // 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));
+                    totalRecords = 0;
+                    return Enumerable.Empty();
                 }
 
-                return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering);
+                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;
 
-        /// 
-        public IEnumerable GetPagedDescendants(IEnumerable ids, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null)
+        var idsA = ids.ToArray();
+        if (idsA.Length == 0)
         {
-            totalRecords = 0;
+            return Enumerable.Empty();
+        }
 
-            var idsA = ids.ToArray();
-            if (idsA.Length == 0)
-                return Enumerable.Empty();
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            Guid objectTypeGuid = objectType.GetGuid();
+            IQuery query = Query();
 
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
+            if (idsA.All(x => x != Constants.System.Root))
             {
-                var objectTypeGuid = objectType.GetGuid();
-                var query = Query();
+                TreeEntityPath[] paths = _entityRepository.GetAllPaths(objectTypeGuid, idsA).ToArray();
+                if (paths.Length == 0)
+                {
+                    totalRecords = 0;
+                    return Enumerable.Empty();
+                }
 
-                if (idsA.All(x => x != Cms.Core.Constants.System.Root))
+                var clauses = new List>>();
+                foreach (var id in idsA)
                 {
-                    var paths = _entityRepository.GetAllPaths(objectTypeGuid, idsA).ToArray();
-                    if (paths.Length == 0)
+                    // if the id is root then don't add any clauses
+                    if (id == Constants.System.Root)
                     {
-                        totalRecords = 0;
-                        return Enumerable.Empty();
+                        continue;
                     }
-                    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));
+                    TreeEntityPath? entityPath = paths.FirstOrDefault(x => x.Id == id);
+                    if (entityPath == null)
+                    {
+                        continue;
                     }
-                    query.WhereAny(clauses);
+
+                    var path = entityPath.Path;
+                    var qid = id;
+                    clauses.Add(x =>
+                        x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar) ||
+                        x.Path.SqlEndsWith("," + qid, TextColumnType.NVarchar));
                 }
 
-                return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering);
+                query.WhereAny(clauses);
             }
-        }
-
-        /// 
-        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);
-            }
+            return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering);
         }
+    }
 
-        /// 
-        public virtual UmbracoObjectTypes GetObjectType(int id)
+    /// 
+    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))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            IQuery query = Query();
+            if (includeTrashed == false)
             {
-                return _entityRepository.GetObjectType(id);
+                query.Where(x => x.Trashed == false);
             }
-        }
 
-        /// 
-        public virtual UmbracoObjectTypes GetObjectType(Guid key)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetObjectType(key);
-            }
+            return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering);
         }
+    }
 
-        /// 
-        public virtual UmbracoObjectTypes GetObjectType(IUmbracoEntity entity)
+    /// 
+    public virtual UmbracoObjectTypes GetObjectType(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            return entity is IEntitySlim light
-                ? ObjectTypes.GetUmbracoObjectType(light.NodeObjectType)
-                : GetObjectType(entity.Id);
+            return _entityRepository.GetObjectType(id);
         }
+    }
 
-        /// 
-        public virtual Type? GetEntityType(int id)
+    /// 
+    public virtual UmbracoObjectTypes GetObjectType(Guid key)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            var objectType = GetObjectType(id);
-            return objectType.GetClrType();
+            return _entityRepository.GetObjectType(key);
         }
+    }
 
-        /// 
-        public Attempt GetId(Guid key, UmbracoObjectTypes objectType)
-        {
-            return _idKeyMap.GetIdForKey(key, objectType);
-        }
+    /// 
+    public virtual UmbracoObjectTypes GetObjectType(IUmbracoEntity entity) =>
+        entity is IEntitySlim light
+            ? ObjectTypes.GetUmbracoObjectType(light.NodeObjectType)
+            : GetObjectType(entity.Id);
 
-        /// 
-        public Attempt GetId(Udi udi)
-        {
-            return _idKeyMap.GetIdForUdi(udi);
-        }
+    /// 
+    public virtual Type? GetEntityType(int id)
+    {
+        UmbracoObjectTypes objectType = GetObjectType(id);
+        return objectType.GetClrType();
+    }
 
-        /// 
-        public Attempt GetKey(int id, UmbracoObjectTypes umbracoObjectType)
+    /// 
+    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 _idKeyMap.GetKeyForId(id, umbracoObjectType);
+            return _entityRepository.GetAllPaths(objectType.GetGuid(), ids);
         }
+    }
 
-        /// 
-        public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params int[]? ids)
-        {
-            var entityType = objectType.GetClrType();
-            GetObjectType(entityType);
+    /// 
+    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(), ids);
-            }
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAllPaths(objectType.GetGuid(), keys);
         }
+    }
 
-        /// 
-        public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params Guid[] keys)
+    /// 
+    public int ReserveId(Guid key)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            var entityType = objectType.GetClrType();
-            GetObjectType(entityType);
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAllPaths(objectType.GetGuid(), keys);
-            }
+            return _entityRepository.ReserveId(key);
         }
+    }
 
-        /// 
-        public int ReserveId(Guid 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))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.ReserveId(key);
-            }
+            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 c91f536b3830..0a744f3f0f72 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.Strings;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     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)
+    {
+        _contentTypeService = contentTypeService;
+        _mediaService = mediaService;
+        _contentService = contentService;
+        _dataTypeService = dataTypeService;
+        _userService = userService;
+        _localizationService = localizationService;
+        _urlSegmentProviders = urlSegmentProviders;
+        _shortStringHelper = shortStringHelper;
+        _propertyEditors = propertyEditors;
+        _configurationEditorJsonSerializer = configurationEditorJsonSerializer;
+    }
+
     /// 
-    /// Serializes entities to XML
+    ///     Exports an IContent item as an XElement.
     /// 
-    internal class EntityXmlSerializer : IEntityXmlSerializer
+    public XElement Serialize(
+        IContent content,
+        bool published,
+        bool withDescendants = false) // TODO: take care of usage! only used for the packager
     {
-        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;
-
-        public EntityXmlSerializer(
-            IContentService contentService,
-            IMediaService mediaService,
-            IDataTypeService dataTypeService,
-            IUserService userService,
-            ILocalizationService localizationService,
-            IContentTypeService contentTypeService,
-            UrlSegmentProviderCollection urlSegmentProviders,
-            IShortStringHelper shortStringHelper,
-            PropertyEditorCollection propertyEditors,
-            IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
-        {
-            _contentTypeService = contentTypeService;
-            _mediaService = mediaService;
-            _contentService = contentService;
-            _dataTypeService = dataTypeService;
-            _userService = userService;
-            _localizationService = localizationService;
-            _urlSegmentProviders = urlSegmentProviders;
-            _shortStringHelper = shortStringHelper;
-            _propertyEditors = propertyEditors;
-            _configurationEditorJsonSerializer = 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) 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)
-                {
-                    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 (content == 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));
+            throw new ArgumentNullException(nameof(content));
+        }
 
-            var nodeName = media.ContentType.Alias.ToSafeAlias(_shortStringHelper);
+        var nodeName = content.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);
+        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("nodeType", media.ContentType.Id));
-            xml.Add(new XAttribute("nodeTypeAlias", media.ContentType.Alias));
+        xml.Add(new XAttribute("creatorName", content.GetCreatorProfile(_userService)?.Name ?? "??"));
 
-            //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("creatorID", content.CreatorId));
+        xml.Add(new XAttribute("writerName", content.GetWriterProfile(_userService)?.Name ?? "??"));
+        xml.Add(new XAttribute("writerID", content.WriterId));
 
-            //xml.Add(new XAttribute("template", 0)); // no template for media
+        xml.Add(new XAttribute("template", content.TemplateId?.ToString(CultureInfo.InvariantCulture) ?? string.Empty));
 
-            onMediaItemSerialized?.Invoke(media, xml);
+        xml.Add(new XAttribute("isPublished", content.Published));
 
-            if (withDescendants)
+        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)
-                {
-                    var children = _mediaService.GetPagedChildren(media.Id, page++, pageSize, out total);
-                    SerializeChildren(children, xml, onMediaItemSerialized);
-                }
+                IEnumerable children =
+                    _contentService.GetPagedChildren(content.Id, page++, pageSize, out total);
+                SerializeChildren(children, xml, published);
             }
-
-            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);
+        return xml;
+    }
 
-            xml.Add(new XAttribute("nodeType", member.ContentType.Id));
-            xml.Add(new XAttribute("nodeTypeAlias", member.ContentType.Alias));
+    /// 
+    ///     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));
+        }
 
-            // what about writer/creator/version?
+        if (_dataTypeService == null)
+        {
+            throw new ArgumentNullException(nameof(_dataTypeService));
+        }
 
-            xml.Add(new XAttribute("loginName", member.Username!));
-            xml.Add(new XAttribute("email", member.Email!));
-            xml.Add(new XAttribute("icon", member.ContentType.Icon!));
+        if (_userService == null)
+        {
+            throw new ArgumentNullException(nameof(_userService));
+        }
 
-            return xml;
+        if (_localizationService == null)
+        {
+            throw new ArgumentNullException(nameof(_localizationService));
         }
 
-        /// 
-        /// 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)
+        if (media == null)
         {
-            var container = new XElement("DataTypes");
-            foreach (var dataTypeDefinition in dataTypeDefinitions)
-            {
-                container.Add(Serialize(dataTypeDefinition));
-            }
-            return container;
+            throw new ArgumentNullException(nameof(media));
         }
 
-        public XElement Serialize(IDataType dataType)
+        if (_urlSegmentProviders == null)
         {
-            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)));
+            throw new ArgumentNullException(nameof(_urlSegmentProviders));
+        }
 
-            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);
+        var nodeName = media.ContentType.Alias.ToSafeAlias(_shortStringHelper);
 
-                folderNames = string.Join("/", folders.Select(x => WebUtility.UrlEncode(x.Name)).ToArray());
-                folderKeys = string.Join("/", folders.Select(x => x.Key).ToArray());
-            }
+        const bool published = false; // always false for media
+        var urlValue = media.GetUrlSegment(_shortStringHelper, _urlSegmentProviders);
+        XElement xml = SerializeContentBase(media, urlValue, nodeName, published);
 
-            if (string.IsNullOrWhiteSpace(folderNames) == false)
-            {
-                xml.Add(new XAttribute("Folders", folderNames));
-                xml.Add(new XAttribute("FolderKeys", folderKeys));
-            }
+        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()));
 
-            return xml;
-        }
+        // xml.Add(new XAttribute("template", 0)); // no template for media
+        onMediaItemSerialized?.Invoke(media, 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)
+        if (withDescendants)
         {
-            var xml = new XElement("DictionaryItems");
-            foreach (var item in dictionaryItem)
+            const int pageSize = 500;
+            var page = 0;
+            var total = long.MaxValue;
+            while (page * pageSize < total)
             {
-                xml.Add(Serialize(item, includeChildren));
+                IEnumerable children = _mediaService.GetPagedChildren(media.Id, page++, pageSize, out total);
+                SerializeChildren(children, xml, onMediaItemSerialized);
             }
-            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);
+        return xml;
+    }
 
-            if (includeChildren)
-            {
-                var children = _localizationService.GetDictionaryItemChildren(dictionaryItem.Key);
-                if (children is not null)
-                {
-                    foreach (var child in children)
-                    {
-                        xml.Add(Serialize(child, true));
-                    }
-                }
-            }
+    /// 
+    ///     Exports an IMember item as an XElement.
+    /// 
+    public XElement Serialize(IMember member)
+    {
+        var nodeName = member.ContentType.Alias.ToSafeAlias(_shortStringHelper);
 
-            return xml;
+        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));
         }
 
-        private XElement Serialize(IDictionaryItem dictionaryItem)
+        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)
         {
-            var xml = new XElement("DictionaryItem",
-                new XAttribute("Key", dictionaryItem.Key),
-                new XAttribute("Name", dictionaryItem.ItemKey));
+            // get URL encoded folder names
+            IOrderedEnumerable folders = _dataTypeService.GetContainers(dataType)
+                .OrderBy(x => x.Level);
 
-            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!)));
-            }
+            folderNames = string.Join("/", folders.Select(x => WebUtility.UrlEncode(x.Name)).ToArray());
+            folderKeys = string.Join("/", folders.Select(x => x.Key).ToArray());
+        }
 
-            return xml;
+        if (string.IsNullOrWhiteSpace(folderNames) == false)
+        {
+            xml.Add(new XAttribute("Folders", folderNames));
+            xml.Add(new XAttribute("FolderKeys", folderKeys));
         }
 
-        public XElement Serialize(IStylesheet stylesheet, bool includeProperties)
+        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)
         {
-            var xml = new XElement("Stylesheet",
-                new XElement("Name", stylesheet.Alias),
-                new XElement("FileName", stylesheet.Path),
-                new XElement("Content", new XCData(stylesheet.Content!)));
+            xml.Add(Serialize(item, includeChildren));
+        }
 
-            if (!includeProperties)
-            {
-                return xml;
-            }
+        return xml;
+    }
 
-            var props = new XElement("Properties");
-            xml.Add(props);
+    /// 
+    ///     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 (stylesheet.Properties is not null)
+        if (includeChildren)
+        {
+            IEnumerable? children = _localizationService.GetDictionaryItemChildren(dictionaryItem.Key);
+            if (children is not null)
             {
-                foreach (var prop in stylesheet.Properties)
+                foreach (IDictionaryItem child in children)
                 {
-                    props.Add(new XElement("Property",
-                        new XElement("Name", prop.Name),
-                        new XElement("Alias", prop.Alias),
-                        new XElement("Value", prop.Value)));
+                    xml.Add(Serialize(child, true));
                 }
             }
+        }
+
+        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;
         }
 
-        /// 
-        /// 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 props = new XElement("Properties");
+        xml.Add(props);
+
+        if (stylesheet.Properties is not null)
         {
-            var xml = new XElement("Languages");
-            foreach (var language in languages)
+            foreach (IStylesheetProperty prop in stylesheet.Properties)
             {
-                xml.Add(Serialize(language));
+                props.Add(new XElement(
+                    "Property",
+                    new XElement("Name", prop.Name),
+                    new XElement("Alias", prop.Alias),
+                    new XElement("Value", prop.Value)));
             }
-            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;
+    }
 
-            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));
         }
 
-        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!)));
+        return xml;
+    }
 
-            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));
-                }
-            }
+    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;
-        }
+        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!)));
 
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// 
-        /// 
-        public XElement Serialize(IEnumerable templates)
+        if (template is Template concreteTemplate && concreteTemplate.MasterTemplateId != null)
         {
-            var xml = new XElement("Templates");
-            foreach (var item in templates)
+            if (concreteTemplate.MasterTemplateId.IsValueCreated &&
+                concreteTemplate.MasterTemplateId.Value != default)
             {
-                xml.Add(Serialize(item));
+                xml.Add(new XElement("Master", concreteTemplate.MasterTemplateId.ToString()));
+                xml.Add(new XElement("MasterAlias", concreteTemplate.MasterTemplateAlias));
             }
-            return xml;
         }
 
+        return xml;
+    }
 
-        public XElement Serialize(IMediaType mediaType)
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// 
+    /// 
+    public XElement Serialize(IEnumerable templates)
+    {
+        var xml = new XElement("Templates");
+        foreach (ITemplate item in templates)
         {
-            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()));
+            xml.Add(Serialize(item));
+        }
 
-            var masterContentType = mediaType.CompositionAliases().FirstOrDefault();
-            if (masterContentType != null)
-            {
-                info.Add(new XElement("Master", masterContentType));
-            }
+        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)
+        var structure = new XElement("Structure");
+        if (mediaType.AllowedContentTypes is not null)
+        {
+            foreach (ContentTypeSort allowedType in mediaType.AllowedContentTypes)
             {
-                foreach (var allowedType in mediaType.AllowedContentTypes)
-                {
-                    structure.Add(new XElement("MediaType", allowedType.Alias));
-                }
+                structure.Add(new XElement("MediaType", allowedType.Alias));
             }
+        }
 
-            var genericProperties = new XElement("GenericProperties", SerializePropertyTypes(mediaType.PropertyTypes, mediaType.PropertyGroups)); // actually, all of them
+        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 tabs = new XElement(
+            "Tabs",
+            SerializePropertyGroups(mediaType.PropertyGroups)); // TODO Rename to PropertyGroups
 
-            var xml = new XElement("MediaType",
-                                   info,
-                                   structure,
-                                   genericProperties,
-                                   tabs);
+        var xml = new XElement(
+            "MediaType",
+            info,
+            structure,
+            genericProperties,
+            tabs);
 
-            return xml;
-        }
+        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)
+    /// 
+    ///     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)
         {
-            var xml = new XElement("Macros");
-            foreach (var item in macros)
-            {
-                xml.Add(Serialize(item));
-            }
-            return xml;
+            xml.Add(Serialize(item));
         }
 
-        public XElement Serialize(IMacro macro)
+        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)
         {
-            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()));
+            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)));
+        }
 
-            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);
+        xml.Add(properties);
 
-            return xml;
-        }
+        return xml;
+    }
 
-        public XElement Serialize(IContentType contentType)
+    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)
         {
-            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()));
+            info.Add(new XElement("Master", masterContentType.Alias));
+        }
 
-            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");
+        IEnumerable compositions = contentType.ContentTypeComposition;
+        foreach (IContentTypeComposition composition in compositions)
+        {
+            compositionsElement.Add(new XElement("Composition", composition.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);
+        info.Add(compositionsElement);
 
-            var allowedTemplates = new XElement("AllowedTemplates");
-            if (contentType.AllowedTemplates is not null)
+        var allowedTemplates = new XElement("AllowedTemplates");
+        if (contentType.AllowedTemplates is not null)
+        {
+            foreach (ITemplate template in contentType.AllowedTemplates)
             {
-                foreach (var template in contentType.AllowedTemplates)
-                {
-                    allowedTemplates.Add(new XElement("Template", template.Alias));
-                }
+                allowedTemplates.Add(new XElement("Template", template.Alias));
             }
+        }
 
-            info.Add(allowedTemplates);
+        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", ""));
-            }
+        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)
+        var structure = new XElement("Structure");
+        if (contentType.AllowedContentTypes is not null)
+        {
+            foreach (ContentTypeSort allowedType in contentType.AllowedContentTypes)
             {
-                foreach (var allowedType in contentType.AllowedContentTypes)
-                {
-                    structure.Add(new XElement("DocumentType", allowedType.Alias));
-                }
+                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 genericProperties = new XElement(
+            "GenericProperties",
+            SerializePropertyTypes(contentType.PropertyTypes, contentType.PropertyGroups)); // actually, all of them
 
-            var xml = new XElement("DocumentType",
-                info,
-                structure,
-                genericProperties,
-                tabs);
+        var tabs = new XElement(
+            "Tabs",
+            SerializePropertyGroups(contentType.PropertyGroups)); // TODO Rename to PropertyGroups
 
-            if (contentType is IContentTypeWithHistoryCleanup withCleanup && withCleanup.HistoryCleanup is not null)
-            {
-                xml.Add(SerializeCleanupPolicy(withCleanup.HistoryCleanup));
-            }
+        var xml = new XElement(
+            "DocumentType",
+            info,
+            structure,
+            genericProperties,
+            tabs);
 
-            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 (contentType is IContentTypeWithHistoryCleanup withCleanup && withCleanup.HistoryCleanup is not null)
+        {
+            xml.Add(SerializeCleanupPolicy(withCleanup.HistoryCleanup));
+        }
 
-            if (string.IsNullOrWhiteSpace(folderNames) == false)
-            {
-                xml.Add(new XAttribute("Folders", folderNames));
-                xml.Add(new XAttribute("FolderKeys", folderKeys));
-            }
+        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);
 
-            return xml;
+            folderNames = string.Join("/", folders.Select(x => WebUtility.UrlEncode(x.Name)).ToArray());
+            folderKeys = string.Join("/", folders.Select(x => x.Key).ToArray());
         }
 
-        private IEnumerable SerializePropertyTypes(IEnumerable propertyTypes, IEnumerable propertyGroups)
+        if (string.IsNullOrWhiteSpace(folderNames) == false)
         {
-            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);
+            xml.Add(new XAttribute("Folders", folderNames));
+            xml.Add(new XAttribute("FolderKeys", folderKeys));
+        }
 
-                XElement genericProperty = SerializePropertyType(propertyType, definition, propertyGroup);
-                genericProperty.Add(new XElement("Variations", propertyType.Variations.ToString()));
+        return xml;
+    }
 
-                yield return genericProperty;
-            }
-        }
+    private XElement Serialize(IDictionaryItem dictionaryItem)
+    {
+        var xml = new XElement(
+            "DictionaryItem",
+            new XAttribute("Key", dictionaryItem.Key),
+            new XAttribute("Name", dictionaryItem.ItemKey));
 
-        private IEnumerable SerializePropertyGroups(IEnumerable propertyGroups)
+        foreach (IDictionaryTranslation translation in dictionaryItem.Translations)
         {
-            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));
-            }
+            xml.Add(new XElement(
+                "Value",
+                new XAttribute("LanguageId", translation.Language!.Id),
+                new XAttribute("LanguageCultureAlias", translation.Language.IsoCode),
+                new XCData(translation.Value)));
         }
 
-        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));
-            }
+        return xml;
+    }
 
-            var element = new XElement("HistoryCleanupPolicy",
-                new XAttribute("preventCleanup", cleanupPolicy.PreventCleanup));
+    private IEnumerable SerializePropertyTypes(
+        IEnumerable propertyTypes,
+        IEnumerable propertyGroups)
+    {
+        foreach (IPropertyType propertyType in propertyTypes)
+        {
+            IDataType? definition = _dataTypeService.GetDataType(propertyType.DataTypeId);
 
-            if (cleanupPolicy.KeepAllVersionsNewerThanDays.HasValue)
-            {
-                element.Add(new XAttribute("keepAllVersionsNewerThanDays", cleanupPolicy.KeepAllVersionsNewerThanDays));
-            }
+            PropertyGroup? propertyGroup = propertyType.PropertyGroupId == null // true generic property
+                ? null
+                : propertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value);
 
-            if (cleanupPolicy.KeepLatestVersionPerDayForDays.HasValue)
-            {
-                element.Add(new XAttribute("keepLatestVersionPerDayForDays", cleanupPolicy.KeepLatestVersionPerDayForDays));
-            }
+            XElement genericProperty = SerializePropertyType(propertyType, definition, propertyGroup);
+            genericProperty.Add(new XElement("Variations", propertyType.Variations.ToString()));
 
-            return element;
+            yield return genericProperty;
         }
+    }
 
-        // exports an IContentBase (IContent, IMedia or IMember) as an XElement.
-        private XElement SerializeContentBase(IContentBase contentBase, string? urlValue, string nodeName, bool published)
+    private IEnumerable SerializePropertyGroups(IEnumerable propertyGroups)
+    {
+        foreach (PropertyGroup propertyGroup in propertyGroups)
         {
-            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", ""));
+            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));
+        }
 
-            // Add culture specific node names
-            foreach (var culture in contentBase.AvailableCultures)
-            {
-                xml.Add(new XAttribute("nodeName-" + culture, contentBase.GetCultureName(culture)!));
-            }
+        var element = new XElement(
+            "HistoryCleanupPolicy",
+            new XAttribute("preventCleanup", cleanupPolicy.PreventCleanup));
 
-            foreach (var property in contentBase.Properties)
-                xml.Add(SerializeProperty(property, published));
+        if (cleanupPolicy.KeepAllVersionsNewerThanDays.HasValue)
+        {
+            element.Add(new XAttribute("keepAllVersionsNewerThanDays", cleanupPolicy.KeepAllVersionsNewerThanDays));
+        }
 
-            return xml;
+        if (cleanupPolicy.KeepLatestVersionPerDayForDays.HasValue)
+        {
+            element.Add(new XAttribute("keepLatestVersionPerDayForDays", cleanupPolicy.KeepLatestVersionPerDayForDays));
         }
 
-        // exports a property as XElements.
-        private IEnumerable SerializeProperty(IProperty property, bool published)
+        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)
         {
-            var propertyType = property.PropertyType;
+            xml.Add(new XAttribute("nodeName-" + culture, contentBase.GetCultureName(culture)!));
+        }
 
-            // 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);
+        foreach (IProperty property in contentBase.Properties)
+        {
+            xml.Add(SerializeProperty(property, published));
         }
 
-        // exports an IContent item descendants.
-        private void SerializeChildren(IEnumerable children, XElement xml, bool 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)
         {
-            foreach (var 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)
             {
-                // 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);
-                }
+                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)
+    // exports an IMedia item descendants.
+    private void SerializeChildren(IEnumerable children, XElement xml, Action? onMediaItemSerialized)
+    {
+        foreach (IMedia child in children)
         {
-            foreach (var 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)
             {
-                // add the child xml
-                var childXml = Serialize(child, onMediaItemSerialized: onMediaItemSerialized);
-                xml.Add(childXml);
-
-                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);
-                }
+                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 d934e895282e..677108dbcd15 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.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;
+    private readonly IExternalLoginWithKeyRepository _externalLoginRepository;
 
-        public ExternalLoginService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IExternalLoginWithKeyRepository externalLoginRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
-        {
-            _externalLoginRepository = 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())
-        {
-        }
+    [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 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 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 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 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());
+    /// 
+    [Obsolete("Use overload that takes a user/member key (Guid).")]
+    public void DeleteUserLogins(int userId)
+        => DeleteUserLogins(userId.ToGuid());
 
-        /// 
-        public IEnumerable GetExternalLogins(Guid userOrMemberKey)
+    public IEnumerable Find(string loginProvider, string providerKey)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _externalLoginRepository.Get(Query().Where(x => x.Key == userOrMemberKey))
-                    .ToList();
-            }
+            return _externalLoginRepository.Get(Query()
+                    .Where(x => x.ProviderKey == providerKey && x.LoginProvider == loginProvider))
+                .ToList();
         }
+    }
 
-        /// 
-        public IEnumerable GetExternalLoginTokens(Guid userOrMemberKey)
+    /// 
+    public IEnumerable GetExternalLogins(Guid userOrMemberKey)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _externalLoginRepository.Get(Query().Where(x => x.Key == userOrMemberKey))
-                    .ToList();
-            }
+            return _externalLoginRepository.Get(Query().Where(x => x.Key == userOrMemberKey))
+                .ToList();
         }
+    }
 
-        /// 
-        public IEnumerable Find(string loginProvider, string providerKey)
+    /// 
+    public IEnumerable GetExternalLoginTokens(Guid userOrMemberKey)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _externalLoginRepository.Get(Query()
-                    .Where(x => x.ProviderKey == providerKey && x.LoginProvider == loginProvider))
-                    .ToList();
-            }
+            return _externalLoginRepository.Get(Query().Where(x => x.Key == userOrMemberKey))
+                .ToList();
         }
+    }
 
-        /// 
-        public void Save(Guid userOrMemberKey, IEnumerable logins)
+    /// 
+    public void Save(Guid userOrMemberKey, IEnumerable logins)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _externalLoginRepository.Save(userOrMemberKey, logins);
-                scope.Complete();
-            }
+            _externalLoginRepository.Save(userOrMemberKey, logins);
+            scope.Complete();
         }
+    }
 
-        /// 
-        public void Save(Guid userOrMemberKey, IEnumerable tokens)
+    /// 
+    public void Save(Guid userOrMemberKey, IEnumerable tokens)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _externalLoginRepository.Save(userOrMemberKey, tokens);
-                scope.Complete();
-            }
+            _externalLoginRepository.Save(userOrMemberKey, tokens);
+            scope.Complete();
         }
+    }
 
-        /// 
-        public void DeleteUserLogins(Guid userOrMemberKey)
+    /// 
+    public void DeleteUserLogins(Guid userOrMemberKey)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _externalLoginRepository.DeleteUserLogins(userOrMemberKey);
-                scope.Complete();
-            }
+            _externalLoginRepository.DeleteUserLogins(userOrMemberKey);
+            scope.Complete();
         }
     }
 }
diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs
index d69276562020..758df3d1026f 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.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 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;
-
-        private const string PartialViewHeader = "@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage";
-        private const string PartialViewMacroHeader = "@inherits Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage";
-
-        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)
-        {
-            _stylesheetRepository = stylesheetRepository;
-            _scriptRepository = scriptRepository;
-            _templateRepository = templateRepository;
-            _partialViewRepository = partialViewRepository;
-            _partialViewMacroRepository = partialViewMacroRepository;
-            _auditRepository = auditRepository;
-            _shortStringHelper = shortStringHelper;
-            _globalSettings = globalSettings.Value;
-            _hostingEnvironment = hostingEnvironment;
-        }
-
-        #region Stylesheets
-
-        /// 
-        public IEnumerable GetStylesheets(params string[] paths)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _stylesheetRepository.GetMany(paths);
-            }
+    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)
+    {
+        _stylesheetRepository = stylesheetRepository;
+        _scriptRepository = scriptRepository;
+        _templateRepository = templateRepository;
+        _partialViewRepository = partialViewRepository;
+        _partialViewMacroRepository = partialViewMacroRepository;
+        _auditRepository = auditRepository;
+        _shortStringHelper = shortStringHelper;
+        _globalSettings = globalSettings.Value;
+        _hostingEnvironment = hostingEnvironment;
+    }
+
+    #region Stylesheets
+
+    /// 
+    public IEnumerable GetStylesheets(params string[] paths)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _stylesheetRepository.GetMany(paths);
         }
+    }
 
-        /// 
-        public IStylesheet? GetStylesheet(string? path)
+    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))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _stylesheetRepository.Get(path);
-            }
+            return _stylesheetRepository.Get(path);
         }
+    }
 
-        /// 
-        public void SaveStylesheet(IStylesheet? stylesheet, int? userId = null)
+    /// 
+    public void SaveStylesheet(IStylesheet? stylesheet, int? userId = null)
+    {
+        if (stylesheet is null)
         {
-            if (stylesheet is null)
+            return;
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            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");
 
-            {
-                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();
-            }
+            scope.Complete();
         }
+    }
 
-        /// 
-        public void DeleteStylesheet(string path, int? userId)
+    /// 
+    public void DeleteStylesheet(string path, int? userId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            IStylesheet? stylesheet = _stylesheetRepository.Get(path);
+            if (stylesheet == null)
             {
-                IStylesheet? stylesheet = _stylesheetRepository.Get(path);
-                if (stylesheet == null)
-                {
-                    scope.Complete();
-                    return;
-                }
+                scope.Complete();
+                return;
+            }
 
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new StylesheetDeletingNotification(stylesheet, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return; // causes rollback
-                }
+            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);
+            userId ??= Constants.Security.SuperUserId;
+            _stylesheetRepository.Delete(stylesheet);
 
-                scope.Notifications.Publish(new StylesheetDeletedNotification(stylesheet, eventMessages).WithStateFrom(deletingNotification));
-                Audit(AuditType.Delete, userId.Value, -1, "Stylesheet");
+            scope.Notifications.Publish(
+                new StylesheetDeletedNotification(stylesheet, eventMessages).WithStateFrom(deletingNotification));
+            Audit(AuditType.Delete, userId.Value, -1, "Stylesheet");
 
-                scope.Complete();
-            }
+            scope.Complete();
         }
+    }
 
-        /// 
-        public void CreateStyleSheetFolder(string folderPath)
+    /// 
+    public void CreateStyleSheetFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _stylesheetRepository.AddFolder(folderPath);
-                scope.Complete();
-            }
+            _stylesheetRepository.AddFolder(folderPath);
+            scope.Complete();
         }
+    }
 
-        /// 
-        public void DeleteStyleSheetFolder(string folderPath)
+    /// 
+    public void DeleteStyleSheetFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _stylesheetRepository.DeleteFolder(folderPath);
-                scope.Complete();
-            }
+            _stylesheetRepository.DeleteFolder(folderPath);
+            scope.Complete();
         }
+    }
 
-        /// 
-        public Stream GetStylesheetFileContentStream(string filepath)
+    /// 
+    public Stream GetStylesheetFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _stylesheetRepository.GetFileContentStream(filepath);
-            }
+            return _stylesheetRepository.GetFileContentStream(filepath);
         }
+    }
 
-        /// 
-        public void SetStylesheetFileContent(string filepath, Stream content)
+    /// 
+    public void SetStylesheetFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _stylesheetRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
+            _stylesheetRepository.SetFileContent(filepath, content);
+            scope.Complete();
         }
+    }
 
-        /// 
-        public long GetStylesheetFileSize(string filepath)
+    /// 
+    public long GetStylesheetFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _stylesheetRepository.GetFileSize(filepath);
-            }
+            return _stylesheetRepository.GetFileSize(filepath);
         }
+    }
 
-        #endregion
+    #endregion
 
-        #region Scripts
+    #region Scripts
 
-        /// 
-        public IEnumerable GetScripts(params string[] names)
+    /// 
+    public IEnumerable GetScripts(params string[] names)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _scriptRepository.GetMany(names);
-            }
+            return _scriptRepository.GetMany(names);
         }
+    }
 
-        /// 
-        public IScript? GetScript(string? name)
+    /// 
+    public IScript? GetScript(string? name)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _scriptRepository.Get(name);
-            }
+            return _scriptRepository.Get(name);
         }
+    }
 
-        /// 
-        public void SaveScript(IScript? script, int? userId)
+    /// 
+    public void SaveScript(IScript? script, int? userId)
+    {
+        if (userId is null)
         {
-            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));
+            userId = Constants.Security.SuperUserId;
+        }
 
-                Audit(AuditType.Save, userId.Value, -1, "Script");
-                scope.Complete();
-            }
+        if (script is null)
+        {
+            return;
         }
 
-        /// 
-        public void DeleteScript(string path, int? userId = null)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new ScriptSavingNotification(script, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                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();
+                return;
             }
+
+            _scriptRepository.Save(script);
+            scope.Notifications.Publish(
+                new ScriptSavedNotification(script, eventMessages).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, userId.Value, -1, "Script");
+            scope.Complete();
         }
+    }
 
-        /// 
-        public void CreateScriptFolder(string folderPath)
+    /// 
+    public void DeleteScript(string path, int? userId = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            IScript? script = _scriptRepository.Get(path);
+            if (script == null)
             {
-                _scriptRepository.AddFolder(folderPath);
                 scope.Complete();
+                return;
             }
-        }
 
-        /// 
-        public void DeleteScriptFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingNotification = new ScriptDeletingNotification(script, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                _scriptRepository.DeleteFolder(folderPath);
                 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 Stream GetScriptFileContentStream(string filepath)
+    /// 
+    public void CreateScriptFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _scriptRepository.GetFileContentStream(filepath);
-            }
+            _scriptRepository.AddFolder(folderPath);
+            scope.Complete();
         }
+    }
 
-        /// 
-        public void SetScriptFileContent(string filepath, Stream content)
+    /// 
+    public void DeleteScriptFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _scriptRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
+            _scriptRepository.DeleteFolder(folderPath);
+            scope.Complete();
         }
+    }
 
-        /// 
-        public long GetScriptFileSize(string filepath)
+    /// 
+    public Stream GetScriptFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _scriptRepository.GetFileSize(filepath);
-            }
+            return _scriptRepository.GetFileContentStream(filepath);
         }
+    }
 
-        #endregion
-
-        #region Templates
+    /// 
+    public void SetScriptFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _scriptRepository.SetFileContent(filepath, content);
+            scope.Complete();
+        }
+    }
 
-        /// 
-        /// Creates a template for a content type
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// The template created
-        /// 
-        public Attempt?> CreateTemplateForContentType(string contentTypeAlias, string? contentTypeName, int userId = Constants.Security.SuperUserId)
+    /// 
+    public long GetScriptFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            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);
+            return _scriptRepository.GetFileSize(filepath);
+        }
+    }
 
-            EventMessages eventMessages = EventMessagesFactory.Get();
+    #endregion
 
-            if (contentTypeAlias != null && contentTypeAlias.Length > 255)
-            {
-                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
-            }
+    #region Templates
 
-            // 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;
-            }
+    /// 
+    ///     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,
 
-            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);
-                }
+            // 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);
 
-                _templateRepository.Save(template);
-                scope.Notifications.Publish(new TemplateSavedNotification(template, eventMessages).WithStateFrom(savingEvent));
+        EventMessages eventMessages = EventMessagesFactory.Get();
 
-                Audit(AuditType.Save, userId, template.Id, ObjectTypes.GetName(UmbracoObjectTypes.Template));
-                scope.Complete();
-            }
+        if (contentTypeAlias != null && contentTypeAlias.Length > 255)
+        {
+            throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+        }
 
-            return OperationResult.Attempt.Succeed(OperationResultType.Success, eventMessages, template);
+        // 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)
+        {
+            template.Content = content;
         }
 
-        /// 
-        /// 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)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            if (name == null)
+            var savingEvent = new TemplateSavingNotification(template, eventMessages, true, contentTypeAlias!);
+            if (scope.Notifications.PublishCancelable(savingEvent))
             {
-                throw new ArgumentNullException(nameof(name));
+                scope.Complete();
+                return OperationResult.Attempt.Fail(
+                    OperationResultType.FailedCancelledByEvent, eventMessages, template);
             }
 
-            if (string.IsNullOrWhiteSpace(name))
-            {
-                throw new ArgumentException("Name cannot be empty or contain only white-space characters", nameof(name));
-            }
+            _templateRepository.Save(template);
+            scope.Notifications.Publish(
+                new TemplateSavedNotification(template, eventMessages).WithStateFrom(savingEvent));
 
-            if (name.Length > 255)
-            {
-                throw new ArgumentOutOfRangeException(nameof(name), "Name cannot be more than 255 characters in length.");
-            }
+            Audit(AuditType.Save, userId, template.Id, UmbracoObjectTypes.Template.GetName());
+            scope.Complete();
+        }
 
-            // 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
-            };
+        return OperationResult.Attempt.Succeed(
+            OperationResultType.Success,
+            eventMessages,
+            template);
+    }
 
-            if (masterTemplate != null)
-            {
-                template.SetMasterTemplate(masterTemplate);
-            }
+    /// 
+    ///     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)
+        {
+            throw new ArgumentNullException(nameof(name));
+        }
 
-            SaveTemplate(template, userId);
+        if (string.IsNullOrWhiteSpace(name))
+        {
+            throw new ArgumentException("Name cannot be empty or contain only white-space characters", nameof(name));
+        }
 
-            return template;
+        if (name.Length > 255)
+        {
+            throw new ArgumentOutOfRangeException(nameof(name), "Name cannot be more than 255 characters in length.");
         }
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        public IEnumerable GetTemplates(params string[] aliases)
+        // 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)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.GetAll(aliases).OrderBy(x => x.Name);
-            }
+            template.SetMasterTemplate(masterTemplate);
         }
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        public IEnumerable GetTemplates(int masterTemplateId)
+        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))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.GetChildren(masterTemplateId).OrderBy(x => x.Name);
-            }
+            return _templateRepository.GetAll(aliases).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)
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    public IEnumerable GetTemplates(int masterTemplateId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.Get(alias);
-            }
+            return _templateRepository.GetChildren(masterTemplateId).OrderBy(x => x.Name);
         }
+    }
 
-        /// 
-        /// Gets a  object by its identifier.
-        /// 
-        /// The identifier of the template.
-        /// The  object matching the identifier, or null.
-        public ITemplate? GetTemplate(int id)
+    /// 
+    ///     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))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.Get(id);
-            }
+            return _templateRepository.Get(alias);
         }
+    }
 
-        /// 
-        /// 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)
+    /// 
+    ///     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))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                IQuery? query = Query().Where(x => x.Key == id);
-                return _templateRepository.Get(query)?.SingleOrDefault();
-            }
+            return _templateRepository.Get(id);
         }
+    }
 
-        /// 
-        /// Gets the template descendants
-        /// 
-        /// 
-        /// 
-        public IEnumerable GetTemplateDescendants(int masterTemplateId)
+    /// 
+    ///     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))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.GetDescendants(masterTemplateId);
-            }
+            IQuery? query = Query().Where(x => x.Key == id);
+            return _templateRepository.Get(query)?.SingleOrDefault();
         }
+    }
 
-        /// 
-        /// Saves a 
-        /// 
-        ///  to save
-        /// 
-        public void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId)
+    /// 
+    ///     Gets the template descendants
+    /// 
+    /// 
+    /// 
+    public IEnumerable GetTemplateDescendants(int masterTemplateId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            if (template == null)
-            {
-                throw new ArgumentNullException(nameof(template));
-            }
+            return _templateRepository.GetDescendants(masterTemplateId);
+        }
+    }
 
-            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.");
-            }
+    /// 
+    ///     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())
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new TemplateSavingNotification(template, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new TemplateSavingNotification(template, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
+                scope.Complete();
+                return;
+            }
 
-                _templateRepository.Save(template);
+            _templateRepository.Save(template);
 
-                scope.Notifications.Publish(new TemplateSavedNotification(template, eventMessages).WithStateFrom(savingNotification));
+            scope.Notifications.Publish(
+                new TemplateSavedNotification(template, eventMessages).WithStateFrom(savingNotification));
 
-                Audit(AuditType.Save, userId, template.Id, UmbracoObjectTypes.Template.GetName());
-                scope.Complete();
-            }
+            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)
+    /// 
+    ///     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())
         {
-            ITemplate[] templatesA = templates.ToArray();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new TemplateSavingNotification(templatesA, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                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;
             }
-        }
 
-        /// 
-        /// 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())
+            foreach (ITemplate template in templatesA)
             {
-                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);
+                _templateRepository.Save(template);
+            }
 
-                scope.Notifications.Publish(new TemplateDeletedNotification(template, eventMessages).WithStateFrom(deletingNotification));
+            scope.Notifications.Publish(
+                new TemplateSavedNotification(templatesA, eventMessages).WithStateFrom(savingNotification));
 
-                Audit(AuditType.Delete, userId, template.Id, ObjectTypes.GetName(UmbracoObjectTypes.Template));
-                scope.Complete();
-            }
+            Audit(AuditType.Save, userId, -1, UmbracoObjectTypes.Template.GetName());
+            scope.Complete();
         }
+    }
 
-        private string? GetViewContent(string? fileName)
+    /// 
+    ///     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())
         {
-            if (fileName.IsNullOrWhiteSpace())
+            ITemplate? template = _templateRepository.Get(alias);
+            if (template == null)
             {
-                throw new ArgumentNullException(nameof(fileName));
+                scope.Complete();
+                return;
             }
 
-            if (!fileName!.EndsWith(".cshtml"))
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingNotification = new TemplateDeletingNotification(template, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                fileName = $"{fileName}.cshtml";
+                scope.Complete();
+                return;
             }
 
-            Stream fs = _templateRepository.GetFileContentStream(fileName);
+            _templateRepository.Delete(template);
 
-            using (var view = new StreamReader(fs))
-            {
-                return view.ReadToEnd().Trim();
-            }
+            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)
+    /// 
+    public Stream GetTemplateFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.GetFileContentStream(filepath);
-            }
+            return _templateRepository.GetFileContentStream(filepath);
         }
+    }
 
-        /// 
-        public void SetTemplateFileContent(string filepath, Stream content)
+    private string? GetViewContent(string? fileName)
+    {
+        if (fileName.IsNullOrWhiteSpace())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _templateRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
+            throw new ArgumentNullException(nameof(fileName));
         }
 
-        /// 
-        public long GetTemplateFileSize(string filepath)
+        if (!fileName!.EndsWith(".cshtml"))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.GetFileSize(filepath);
-            }
+            fileName = $"{fileName}.cshtml";
         }
 
-        #endregion
+        Stream fs = _templateRepository.GetFileContentStream(fileName);
+
+        using (var view = new StreamReader(fs))
+        {
+            return view.ReadToEnd().Trim();
+        }
+    }
 
-        #region Partial Views
+    /// 
+    public void SetTemplateFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _templateRepository.SetFileContent(filepath, content);
+            scope.Complete();
+        }
+    }
 
-        public IEnumerable GetPartialViewSnippetNames(params string[] filterNames)
+    /// 
+    public long GetTemplateFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            var snippetProvider =
-                new EmbeddedFileProvider(this.GetType().Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets");
+            return _templateRepository.GetFileSize(filepath);
+        }
+    }
 
-            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();
+    #endregion
 
-            //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();
+    #region Partial Views
 
-            return empty.Union(files.Except(empty)).WhereNotNull();
-        }
+    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)
+    public void DeletePartialViewFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewRepository.DeleteFolder(folderPath);
-                scope.Complete();
-            }
+            _partialViewRepository.DeleteFolder(folderPath);
+            scope.Complete();
         }
+    }
 
-        public void DeletePartialViewMacroFolder(string folderPath)
+    public void DeletePartialViewMacroFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewMacroRepository.DeleteFolder(folderPath);
-                scope.Complete();
-            }
+            _partialViewMacroRepository.DeleteFolder(folderPath);
+            scope.Complete();
         }
+    }
 
-        public IEnumerable GetPartialViews(params string[] names)
+    public IEnumerable GetPartialViews(params string[] names)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewRepository.GetMany(names);
-            }
+            return _partialViewRepository.GetMany(names);
         }
+    }
 
-        public IPartialView? GetPartialView(string path)
+    public IPartialView? GetPartialView(string path)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewRepository.Get(path);
-            }
+            return _partialViewRepository.Get(path);
         }
+    }
 
-        public IPartialView? GetPartialViewMacro(string path)
+    public IPartialView? GetPartialViewMacro(string path)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewMacroRepository.Get(path);
-            }
+            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 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 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)
+    private Attempt CreatePartialViewMacro(
+        IPartialView partialView,
+        PartialViewType partialViewType,
+        string? snippetName = null,
+        int? userId = Constants.Security.SuperUserId)
+    {
+        string partialViewHeader;
+        switch (partialViewType)
         {
-            string partialViewHeader;
-            switch (partialViewType)
-            {
-                case PartialViewType.PartialView:
-                    partialViewHeader = PartialViewHeader;
-                    break;
-                case PartialViewType.PartialViewMacro:
-                    partialViewHeader = PartialViewMacroHeader;
-                    break;
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(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)
+        string? partialViewContent = null;
+        if (snippetName.IsNullOrWhiteSpace() == false)
+        {
+            // create the file
+            Attempt snippetPathAttempt = TryGetSnippetPath(snippetName);
+            if (snippetPathAttempt.Success == 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}";
-                }
+                throw new InvalidOperationException("Could not load snippet with name " + snippetName);
             }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            using (var snippetFile = new StreamReader(File.OpenRead(snippetPathAttempt.Result!)))
             {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var creatingNotification = new PartialViewCreatingNotification(partialView, eventMessages);
-                if (scope.Notifications.PublishCancelable(creatingNotification))
-                {
-                    scope.Complete();
-                    return Attempt.Fail();
-                }
+                var snippetContent = snippetFile.ReadToEnd().Trim();
+
+                // strip the @inherits if it's there
+                snippetContent = StripPartialViewHeader(snippetContent);
 
-                IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
-                if (partialViewContent != null)
+                // Update Model.Content. to be Model. when used as PartialView
+                if (partialViewType == PartialViewType.PartialView)
                 {
-                    partialView.Content = partialViewContent;
+                    snippetContent = snippetContent.Replace("Model.Content.", "Model.");
                 }
 
-                repository.Save(partialView);
-
-                scope.Notifications.Publish(new PartialViewCreatedNotification(partialView, eventMessages).WithStateFrom(creatingNotification));
-
-                Audit(AuditType.Save, userId!.Value, -1, partialViewType.ToString());
-
-                scope.Complete();
+                partialViewContent = $"{partialViewHeader}{Environment.NewLine}{snippetContent}";
             }
-
-            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())
         {
-            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);
-                IPartialView? partialView = repository.Get(path);
-                if (partialView == null)
-                {
-                    scope.Complete();
-                    return true;
-                }
+            IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
+            if (partialViewContent != null)
+            {
+                partialView.Content = partialViewContent;
+            }
 
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new PartialViewDeletingNotification(partialView, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return false;
-                }
+            repository.Save(partialView);
 
-                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.Notifications.Publish(
+                new PartialViewCreatedNotification(partialView, eventMessages).WithStateFrom(creatingNotification));
 
-                scope.Complete();
-            }
+            Audit(AuditType.Save, userId!.Value, -1, partialViewType.ToString());
 
-            return true;
+            scope.Complete();
         }
 
-        public Attempt SavePartialView(IPartialView partialView, int? userId = null) =>
-            SavePartialView(partialView, PartialViewType.PartialView, userId);
+        return Attempt.Succeed(partialView);
+    }
+
+    public bool DeletePartialViewMacro(string path, int? userId = null) =>
+        DeletePartialViewMacro(path, PartialViewType.PartialViewMacro, userId);
 
-        public Attempt SavePartialViewMacro(IPartialView partialView, int? userId = null) =>
-            SavePartialView(partialView, PartialViewType.PartialViewMacro, userId);
+    public Attempt SavePartialView(IPartialView partialView, int? userId = null) =>
+        SavePartialView(partialView, PartialViewType.PartialView, userId);
 
-        private Attempt SavePartialView(IPartialView partialView, PartialViewType partialViewType, int? userId = null)
+    private bool DeletePartialViewMacro(string path, PartialViewType partialViewType, int? userId = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
+            IPartialView? partialView = repository.Get(path);
+            if (partialView == null)
             {
-                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 true;
+            }
 
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingNotification = new PartialViewDeletingNotification(partialView, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
+            {
                 scope.Complete();
+                return false;
             }
 
-            return Attempt.Succeed(partialView);
+            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();
         }
 
-        internal string StripPartialViewHeader(string contents)
+        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())
         {
-            var headerMatch = new Regex("^@inherits\\s+?.*$", RegexOptions.Multiline);
-            return headerMatch.Replace(contents, string.Empty);
+            _partialViewRepository.AddFolder(folderPath);
+            scope.Complete();
         }
+    }
 
-        internal Attempt TryGetSnippetPath(string? fileName)
+    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())
         {
-            if (fileName?.EndsWith(".cshtml") == false)
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new PartialViewSavingNotification(partialView, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                fileName += ".cshtml";
+                scope.Complete();
+                return Attempt.Fail();
             }
 
-            var snippetPath = _hostingEnvironment.MapPathContentRoot($"{Constants.SystemDirectories.Umbraco}/PartialViewMacros/Templates/{fileName}");
-            return System.IO.File.Exists(snippetPath)
-                ? Attempt.Succeed(snippetPath)
-                : 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();
         }
 
-        public void CreatePartialViewFolder(string folderPath)
+        return Attempt.Succeed(partialView);
+    }
+
+    internal Attempt TryGetSnippetPath(string? fileName)
+    {
+        if (fileName?.EndsWith(".cshtml") == false)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewRepository.AddFolder(folderPath);
-                scope.Complete();
-            }
+            fileName += ".cshtml";
         }
 
-        public void CreatePartialViewMacroFolder(string folderPath)
+        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())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewMacroRepository.AddFolder(folderPath);
-                scope.Complete();
-            }
+            _partialViewMacroRepository.AddFolder(folderPath);
+            scope.Complete();
         }
+    }
 
-        private IPartialViewRepository GetPartialViewRepository(PartialViewType partialViewType)
+    /// 
+    public Stream GetPartialViewFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            switch (partialViewType)
-            {
-                case PartialViewType.PartialView:
-                    return _partialViewRepository;
-                case PartialViewType.PartialViewMacro:
-                    return _partialViewMacroRepository;
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(partialViewType));
-            }
+            return _partialViewRepository.GetFileContentStream(filepath);
         }
+    }
 
-        /// 
-        public Stream GetPartialViewFileContentStream(string filepath)
+    private IPartialViewRepository GetPartialViewRepository(PartialViewType partialViewType)
+    {
+        switch (partialViewType)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewRepository.GetFileContentStream(filepath);
-            }
+            case PartialViewType.PartialView:
+                return _partialViewRepository;
+            case PartialViewType.PartialViewMacro:
+                return _partialViewMacroRepository;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(partialViewType));
         }
+    }
 
-        /// 
-        public void SetPartialViewFileContent(string filepath, Stream content)
+    /// 
+    public void SetPartialViewFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
+            _partialViewRepository.SetFileContent(filepath, content);
+            scope.Complete();
         }
+    }
 
-        /// 
-        public long GetPartialViewFileSize(string filepath)
+    /// 
+    public long GetPartialViewFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewRepository.GetFileSize(filepath);
-            }
+            return _partialViewRepository.GetFileSize(filepath);
         }
+    }
 
-        /// 
-        public Stream GetPartialViewMacroFileContentStream(string filepath)
+    /// 
+    public Stream GetPartialViewMacroFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewMacroRepository.GetFileContentStream(filepath);
-            }
+            return _partialViewMacroRepository.GetFileContentStream(filepath);
         }
+    }
 
-        /// 
-        public void SetPartialViewMacroFileContent(string filepath, Stream content)
+    /// 
+    public void SetPartialViewMacroFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewMacroRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
+            _partialViewMacroRepository.SetFileContent(filepath, content);
+            scope.Complete();
         }
+    }
 
-        /// 
-        public long GetPartialViewMacroFileSize(string filepath)
+    /// 
+    public long GetPartialViewMacroFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewMacroRepository.GetFileSize(filepath);
-            }
+            return _partialViewMacroRepository.GetFileSize(filepath);
         }
+    }
 
-        #endregion
+    #endregion
 
-        #region Snippets
+    #region Snippets
 
-        public string GetPartialViewSnippetContent(string snippetName) => GetPartialViewMacroSnippetContent(snippetName, PartialViewType.PartialView);
+    public string GetPartialViewSnippetContent(string snippetName) =>
+        GetPartialViewMacroSnippetContent(snippetName, PartialViewType.PartialView);
 
-        public string GetPartialViewMacroSnippetContent(string snippetName) => GetPartialViewMacroSnippetContent(snippetName, PartialViewType.PartialViewMacro);
+    public string GetPartialViewMacroSnippetContent(string snippetName) =>
+        GetPartialViewMacroSnippetContent(snippetName, PartialViewType.PartialViewMacro);
 
-        private string GetPartialViewMacroSnippetContent(string snippetName, PartialViewType partialViewType)
+    private string GetPartialViewMacroSnippetContent(string snippetName, PartialViewType partialViewType)
+    {
+        if (snippetName.IsNullOrWhiteSpace())
         {
-            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));
-            }
+            throw new ArgumentNullException(nameof(snippetName));
+        }
 
-            var snippetProvider =
-                new EmbeddedFileProvider(this.GetType().Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets");
+        string partialViewHeader;
+        switch (partialViewType)
+        {
+            case PartialViewType.PartialView:
+                partialViewHeader = PartialViewHeader;
+                break;
+            case PartialViewType.PartialViewMacro:
+                partialViewHeader = PartialViewMacroHeader;
+                break;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(partialViewType));
+        }
 
-            var file = snippetProvider.GetDirectoryContents(string.Empty).FirstOrDefault(x=>x.Exists && x.Name.Equals(snippetName + ".cshtml"));
+        var snippetProvider =
+            new EmbeddedFileProvider(GetType().Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets");
 
-            // Try and get the snippet path
-            if (file is null)
-            {
-                throw new InvalidOperationException("Could not load snippet with name " + snippetName);
-            }
+        IFileInfo? file = snippetProvider.GetDirectoryContents(string.Empty)
+            .FirstOrDefault(x => x.Exists && x.Name.Equals(snippetName + ".cshtml"));
 
-            using (var snippetFile = new StreamReader(file.CreateReadStream()))
-            {
-                var snippetContent = snippetFile.ReadToEnd().Trim();
+        // Try and get the snippet path
+        if (file is null)
+        {
+            throw new InvalidOperationException("Could not load snippet with name " + snippetName);
+        }
 
-                //strip the @inherits if it's there
-                snippetContent = StripPartialViewHeader(snippetContent);
+        using (var snippetFile = new StreamReader(file.CreateReadStream()))
+        {
+            var snippetContent = snippetFile.ReadToEnd().Trim();
 
-                //Update Model.Content to be Model when used as PartialView
-                if (partialViewType == PartialViewType.PartialView)
-                {
-                    snippetContent = snippetContent
-                        .Replace("Model.Content.", "Model.")
-                        .Replace("(Model.Content)", "(Model)");
-                }
+            // strip the @inherits if it's there
+            snippetContent = StripPartialViewHeader(snippetContent);
 
-                var content = $"{partialViewHeader}{Environment.NewLine}{snippetContent}";
-                return content;
+            // Update Model.Content to be Model when used as PartialView
+            if (partialViewType == PartialViewType.PartialView)
+            {
+                snippetContent = snippetContent
+                    .Replace("Model.Content.", "Model.")
+                    .Replace("(Model.Content)", "(Model)");
             }
-        }
 
-        #endregion
+            var content = $"{partialViewHeader}{Environment.NewLine}{snippetContent}";
+            return content;
+        }
+    }
 
-        private void Audit(AuditType type, int userId, int objectId, string? entityType) => _auditRepository.Save(new AuditItem(objectId, type, userId, entityType));
+    #endregion
 
-        // TODO: Method to change name and/or alias of view template
-    }
+    // 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 df816960a3d2..f58da5317448 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
 {
-    /// 
-    /// 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);
+    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);
+    IEnumerable GetLogs(int objectId);
 
-        /// 
-        /// 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);
+    IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = 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);
+    IEnumerable GetLogs(AuditType type, DateTime? sinceDate = 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);
+    void CleanLogs(int maximumAgeOfLogsInMinutes);
+
+    /// 
+    ///     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);
 }
diff --git a/src/Umbraco.Core/Services/IBasicAuthService.cs b/src/Umbraco.Core/Services/IBasicAuthService.cs
index 82e48e11802f..c371376f8552 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
+namespace Umbraco.Cms.Core.Services;
+
+public interface IBasicAuthService
 {
-    public interface IBasicAuthService
-    {
-        bool IsBasicAuthEnabled();
-        bool IsIpAllowListed(IPAddress clientIpAddress);
-        bool HasCorrectSharedSecret(IDictionary headers) => false;
+    bool IsBasicAuthEnabled();
+    bool IsIpAllowListed(IPAddress clientIpAddress);
+    bool HasCorrectSharedSecret(IDictionary headers) => false;
 
-        bool IsRedirectToLoginPageEnabled() => false;
-    }
+    bool IsRedirectToLoginPageEnabled() => false;
 }
diff --git a/src/Umbraco.Core/Services/ICacheInstructionService.cs b/src/Umbraco.Core/Services/ICacheInstructionService.cs
index c884b8bed87d..0b71bde66d32 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 04d81d7f8857..fe044362b764 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 d191caebe298..dc0400850374 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 93d51da7573a..1eb2db83bfdc 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
+
+    /// 
+    ///     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
+
+    #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
+
     /// 
-    /// Defines the ContentService, which is an easy access to operations involving 
-    /// 
-    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
-
-    }
+    ///     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 1916fb49c4ac..1e07da7d8f91 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
+namespace Umbraco.Cms.Core.Services;
+
+public interface IContentServiceBase : IContentServiceBase
+    where TItem : class, IContentBase
 {
-    public interface IContentServiceBase : IContentServiceBase
-        where TItem: class, IContentBase
-    {
-        TItem? GetById(Guid key);
-        Attempt Save(IEnumerable contents, int userId = Constants.Security.SuperUserId);
-    }
+    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
+{
     /// 
-    /// Placeholder for sharing logic between the content, media (and member) services
-    /// TODO: Start sharing the logic!
+    ///     Checks/fixes the data integrity of node paths/levels stored in the database
     /// 
-    public interface IContentServiceBase : IService
-    {
-        /// 
-        /// Checks/fixes the data integrity of node paths/levels stored in the database
-        /// 
-        ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options);
-    }
+    ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options);
 }
diff --git a/src/Umbraco.Core/Services/IContentTypeBaseServiceProvider.cs b/src/Umbraco.Core/Services/IContentTypeBaseServiceProvider.cs
index 4b6a78850c3a..be8cef8fd12d 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 4b34baa8693a..d38139349b2c 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 5614d87bf3e2..8e67c78a201f 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
+{
+    /// 
+    ///     Gets a content type.
+    /// 
+    IContentTypeComposition? Get(int id);
+}
+
+/// 
+///     Provides a common base interface for ,  and
+///     .
+/// 
+/// The type of the item.
+public interface IContentTypeBaseService : IContentTypeBaseService, IService
+    where TItem : IContentTypeComposition
 {
     /// 
-    /// Provides a common base interface for .
+    ///     Gets a content type.
     /// 
-    public interface IContentTypeBaseService
-    {
-        /// 
-        /// Gets a content type.
-        /// 
-        IContentTypeComposition? Get(int id);
-    }
+    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);
-
-        /// 
-        /// Gets a content type.
-        /// 
-        TItem? Get(Guid key);
-
-        /// 
-        /// Gets a content type.
-        /// 
-        TItem? Get(string alias);
-
-        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);
-
-        IEnumerable GetAll(params int[] ids);
-        IEnumerable GetAll(IEnumerable? ids);
-
-        IEnumerable GetDescendants(int id, bool andSelf); // parent-child axis
-        IEnumerable GetComposedOf(int id); // composition axis
-
-        IEnumerable GetChildren(int id);
-        IEnumerable GetChildren(Guid id);
-
-        bool HasChildren(int id);
-        bool HasChildren(Guid 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);
-
-
-        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);
-    }
+    TItem? Get(Guid key);
+
+    /// 
+    ///     Gets a content type.
+    /// 
+    TItem? Get(string alias);
+
+    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);
+
+    IEnumerable GetAll(params int[] ids);
+
+    IEnumerable GetAll(IEnumerable? ids);
+
+    IEnumerable GetDescendants(int id, bool andSelf); // parent-child axis
+
+    IEnumerable GetComposedOf(int id); // composition axis
+
+    IEnumerable GetChildren(int id);
+
+    IEnumerable GetChildren(Guid id);
+
+    bool HasChildren(int id);
+
+    bool HasChildren(Guid 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);
+
+    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 86e298830749..d9cbcc0cda5b 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 d0f203b2ef7b..e0d518f52a7b 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 70e34106276f..2792b142feea 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
-{
-    public interface IDashboardService
-    {
-        /// 
-        /// Gets dashboard for a specific section/application
-        /// For a specific backoffice user
-        /// 
-        /// 
-        /// 
-        /// 
-        IEnumerable> GetDashboards(string section, IUser? currentUser);
+namespace Umbraco.Cms.Core.Services;
 
-        /// 
-        /// Gets all dashboards, organized by section, for a user.
-        /// 
-        /// 
-        /// 
-        IDictionary>> GetDashboards(IUser? currentUser);
+public interface IDashboardService
+{
+    /// 
+    ///     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);
 }
diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs
index 898b24355eda..effb4573b45b 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);
+
+    /// 
+    ///     Gets a  by its Name
+    /// 
+    /// Name of the 
+    /// 
+    ///     
+    /// 
+    IDataType? GetDataType(string name);
+
+    /// 
+    ///     Gets a  by its Id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    IDataType? GetDataType(int id);
 
     /// 
-    /// Defines the DataType Service, which is an easy access to operations involving 
+    ///     Gets a  by its unique guid Id
     /// 
-    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);
-
-        /// 
-        /// Gets a  by its Name
-        /// 
-        /// Name of the 
-        /// 
-        IDataType? GetDataType(string name);
-
-        /// 
-        /// Gets a  by its Id
-        /// 
-        /// Id of the 
-        /// 
-        IDataType? GetDataType(int id);
-
-        /// 
-        /// Gets a  by its unique guid Id
-        /// 
-        /// Unique guid Id of the DataType
-        /// 
-        IDataType? GetDataType(Guid 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);
-
-        /// 
-        /// Saves an 
-        /// 
-        ///  to save
-        /// Id of the user issuing the save
-        void Save(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);
-
-        /// 
-        /// 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);
-    }
+    /// Unique guid Id of the DataType
+    /// 
+    ///     
+    /// 
+    IDataType? GetDataType(Guid 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);
+
+    /// 
+    ///     Saves an 
+    /// 
+    ///  to save
+    /// Id of the user issuing the save
+    void Save(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);
+
+    /// 
+    ///     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);
 }
diff --git a/src/Umbraco.Core/Services/IDomainService.cs b/src/Umbraco.Core/Services/IDomainService.cs
index 952eaecfde43..54a006ecb1f9 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 8dc1210d11c7..1a37045490b6 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 66298aba1d00..74a416a8fe2d 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 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 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 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 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.
-        /// 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.
-        /// 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.
-        /// 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 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 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.
-        /// 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.
-        /// 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.
-        /// 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 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);
-
-        // 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);
-
-        /// 
-        /// Gets the object type of an entity.
-        /// 
-        UmbracoObjectTypes GetObjectType(int id);
-
-        /// 
-        /// Gets the object type of an entity.
-        /// 
-        UmbracoObjectTypes GetObjectType(Guid key);
-
-        /// 
-        /// Gets the object type of an entity.
-        /// 
-        UmbracoObjectTypes GetObjectType(IUmbracoEntity entity);
-
-        /// 
-        /// Gets the CLR type of an entity.
-        /// 
-        Type? GetEntityType(int id);
-
-        /// 
-        /// Gets the integer identifier corresponding to a unique Guid identifier.
-        /// 
-        Attempt GetId(Guid key, UmbracoObjectTypes objectType);
-
-        /// 
-        /// Gets the integer identifier corresponding to a Udi.
-        /// 
-        Attempt GetId(Udi udi);
-
-        /// 
-        /// 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 int[]? ids);
-
-        /// 
-        /// 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);
-    }
+    /// 
+    ///     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 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 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;
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     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 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.
+    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 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(Guid objectType, params Guid[] keys);
+
+    /// 
+    ///     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.
+    /// 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.
+    /// 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.
+    /// 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 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);
+
+    // 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);
+
+    /// 
+    ///     Gets the object type of an entity.
+    /// 
+    UmbracoObjectTypes GetObjectType(int id);
+
+    /// 
+    ///     Gets the object type of an entity.
+    /// 
+    UmbracoObjectTypes GetObjectType(Guid key);
+
+    /// 
+    ///     Gets the object type of an entity.
+    /// 
+    UmbracoObjectTypes GetObjectType(IUmbracoEntity entity);
+
+    /// 
+    ///     Gets the CLR type of an entity.
+    /// 
+    Type? GetEntityType(int id);
+
+    /// 
+    ///     Gets the integer identifier corresponding to a unique Guid identifier.
+    /// 
+    Attempt GetId(Guid key, UmbracoObjectTypes objectType);
+
+    /// 
+    ///     Gets the integer identifier corresponding to a Udi.
+    /// 
+    Attempt GetId(Udi udi);
+
+    /// 
+    ///     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 int[]? ids);
+
+    /// 
+    ///     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 fd68a9dfca66..5ada7ab5b6bf 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.
+    /// 
+    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 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);
+
+    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 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);
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// 
+    /// 
+    XElement Serialize(IEnumerable templates);
+
+    XElement Serialize(IMediaType mediaType);
+
+    /// 
+    ///     Exports a list of  items to xml as an 
     /// 
-    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
-            ;
-
-        /// 
-        /// 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 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);
-
-        /// 
-        /// 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);
-
-        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);
-
-        XElement Serialize(ILanguage language);
-        XElement Serialize(ITemplate template);
-
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// 
-        /// 
-        XElement Serialize(IEnumerable templates);
-
-        XElement Serialize(IMediaType mediaType);
-
-        /// 
-        /// 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);
-    }
+    /// 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 05c5f7d5548f..8d85e17e04c5 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 75f8069f0c2b..ba75d505ffa5 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 bc31f54f8b26..54f827c899a1 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 baccd5dedfb5..3179a4491fe6 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);
+
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetPartialViews(params string[] names);
+
+    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);
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     Gets the size of a partial view.
+    /// 
+    /// The filesystem path to the partial view.
+    /// The size of the partial view.
+    long GetPartialViewFileSize(string filepath);
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     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 a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetStylesheets(params string[] paths);
+
+    /// 
+    ///     Gets a  object by its name
+    /// 
+    /// Path of the stylesheet incl. extension
+    /// A  object
+    IStylesheet? GetStylesheet(string? path);
+
+    /// 
+    ///     Saves a 
+    /// 
+    ///  to save
+    /// Optional id of the user saving the stylesheet
+    void SaveStylesheet(IStylesheet? stylesheet, int? userId = null);
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     Creates a folder for style sheets
+    /// 
+    /// 
+    /// 
+    void CreateStyleSheetFolder(string folderPath);
+
     /// 
-    /// 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);
-
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetPartialViews(params string[] names);
-
-        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);
-
-        /// 
-        /// 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);
-
-        /// 
-        /// 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);
-
-        /// 
-        /// Gets the size of a partial view.
-        /// 
-        /// The filesystem path to the partial view.
-        /// The size of the partial view.
-        long GetPartialViewFileSize(string filepath);
-
-        /// 
-        /// 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);
-
-        /// 
-        /// 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);
-
-        /// 
-        /// 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 a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetStylesheets(params string[] paths);
-
-        /// 
-        /// Gets a  object by its name
-        /// 
-        /// Path of the stylesheet incl. extension
-        /// A  object
-        IStylesheet? GetStylesheet(string? path);
-
-        /// 
-        /// Saves a 
-        /// 
-        ///  to save
-        /// Optional id of the user saving the stylesheet
-        void SaveStylesheet(IStylesheet? stylesheet, int? userId = null);
-
-        /// 
-        /// 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);
-
-        /// 
-        /// Creates a folder for style sheets
-        /// 
-        /// 
-        /// 
-        void CreateStyleSheetFolder(string folderPath);
-
-        /// 
-        /// Deletes a folder for style sheets
-        /// 
-        /// 
-        void DeleteStyleSheetFolder(string folderPath);
-
-        /// 
-        /// Gets the content of a stylesheet as a stream.
-        /// 
-        /// The filesystem path to the stylesheet.
-        /// The content of the stylesheet.
-        Stream GetStylesheetFileContentStream(string filepath);
-
-        /// 
-        /// Sets the content of a stylesheet.
-        /// 
-        /// The filesystem path to the stylesheet.
-        /// The content of the stylesheet.
-        void SetStylesheetFileContent(string filepath, Stream content);
-
-        /// 
-        /// Gets the size of a stylesheet.
-        /// 
-        /// The filesystem path to the stylesheet.
-        /// The size of the stylesheet.
-        long GetStylesheetFileSize(string filepath);
-
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetScripts(params string[] names);
-
-        /// 
-        /// Gets a  object by its name
-        /// 
-        /// Name of the script incl. extension
-        /// A  object
-        IScript? GetScript(string? name);
-
-        /// 
-        /// Saves a 
-        /// 
-        ///  to save
-        /// Optional id of the user saving the script
-        void SaveScript(IScript? script, int? userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// 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);
-
-        /// 
-        /// Creates a folder for scripts
-        /// 
-        /// 
-        /// 
-        void CreateScriptFolder(string folderPath);
-
-        /// 
-        /// Deletes a folder for scripts
-        /// 
-        /// 
-        void DeleteScriptFolder(string folderPath);
-
-        /// 
-        /// 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);
-
-        /// 
-        /// 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 size of a script file.
-        /// 
-        /// The filesystem path to the script file.
-        /// The size of the script file.
-        long GetScriptFileSize(string filepath);
-
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetTemplates(params string[] aliases);
-
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetTemplates(int masterTemplateId);
-
-        /// 
-        /// Gets a  object by its alias.
-        /// 
-        /// The alias of the template.
-        /// The  object matching the alias, or null.
-        ITemplate? GetTemplate(string? alias);
-
-        /// 
-        /// Gets a  object by its identifier.
-        /// 
-        /// The identifier of the template.
-        /// The  object matching the identifier, or null.
-        ITemplate? GetTemplate(int id);
-
-        /// 
-        /// 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 template descendants
-        /// 
-        /// 
-        /// 
-        IEnumerable GetTemplateDescendants(int masterTemplateId);
-
-        /// 
-        /// Saves a 
-        /// 
-        ///  to save
-        /// Optional id of the user saving the template
-        void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Creates a template for a content type
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// The template created
-        /// 
-        Attempt?> CreateTemplateForContentType(string contentTypeAlias, string? contentTypeName, int userId = Constants.Security.SuperUserId);
-
-        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.")]
+    ///     Deletes a folder for style sheets
+    /// 
+    /// 
+    void DeleteStyleSheetFolder(string folderPath);
+
+    /// 
+    ///     Gets the content of a stylesheet as a stream.
+    /// 
+    /// The filesystem path to the stylesheet.
+    /// The content of the stylesheet.
+    Stream GetStylesheetFileContentStream(string filepath);
+
+    /// 
+    ///     Sets the content of a stylesheet.
+    /// 
+    /// The filesystem path to the stylesheet.
+    /// The content of the stylesheet.
+    void SetStylesheetFileContent(string filepath, Stream content);
+
+    /// 
+    ///     Gets the size of a stylesheet.
+    /// 
+    /// The filesystem path to the stylesheet.
+    /// The size of the stylesheet.
+    long GetStylesheetFileSize(string filepath);
+
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetScripts(params string[] names);
+
+    /// 
+    ///     Gets a  object by its name
+    /// 
+    /// Name of the script incl. extension
+    /// A  object
+    IScript? GetScript(string? name);
+
+    /// 
+    ///     Saves a 
+    /// 
+    ///  to save
+    /// Optional id of the user saving the script
+    void SaveScript(IScript? script, int? userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     Creates a folder for scripts
+    /// 
+    /// 
+    /// 
+    void CreateScriptFolder(string folderPath);
+
+    /// 
+    ///     Deletes a folder for scripts
+    /// 
+    /// 
+    void DeleteScriptFolder(string folderPath);
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     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 size of a script file.
+    /// 
+    /// The filesystem path to the script file.
+    /// The size of the script file.
+    long GetScriptFileSize(string filepath);
+
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetTemplates(params string[] aliases);
+
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetTemplates(int masterTemplateId);
+
+    /// 
+    ///     Gets a  object by its alias.
+    /// 
+    /// The alias of the template.
+    /// The  object matching the alias, or null.
+    ITemplate? GetTemplate(string? alias);
+
+    /// 
+    ///     Gets a  object by its identifier.
+    /// 
+    /// The identifier of the template.
+    /// The  object matching the identifier, or null.
+    ITemplate? GetTemplate(int id);
+
+    /// 
+    ///     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 template descendants
+    /// 
+    /// 
+    /// 
+    IEnumerable GetTemplateDescendants(int masterTemplateId);
+
+    /// 
+    ///     Saves a 
+    /// 
+    ///  to save
+    /// Optional id of the user saving the template
+    void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Creates a template for a content type
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     The template created
+    /// 
+    Attempt?> CreateTemplateForContentType(
+        string contentTypeAlias,
+        string? contentTypeName,
+        int userId = Constants.Security.SuperUserId);
+
+    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 0b215c481cc8..8aff7e8920ea 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
+namespace Umbraco.Cms.Core.Services;
+
+public interface IIconService
 {
-    public interface IIconService
-    {
-        /// 
-        /// Gets the svg string for the icon name found at the global icons path
-        /// 
-        /// 
-        /// 
-        IconModel? GetIcon(string iconName);
+    /// 
+    ///     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();
-    }
+    /// 
+    ///     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 199ee23813d9..e85095d41fc7 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 5b1d28cccc34..688c6298bd32 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 7c68bcfa9fe1..f6c371724416 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 1ebf6e972886..97316911c464 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 eca2a8e07029..7a1b1b6fd179 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)
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     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 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 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();
+
+    /// 
+    ///     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);
+
     /// 
-    /// 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)
-
-        /// 
-        /// 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);
-
-        /// 
-        /// 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 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 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();
-
-        /// 
-        /// 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);
-
-        /// 
-        /// 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 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 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 identifier.
-        /// 
-        /// 
-        /// This can be optimized and bypass all deep cloning.
-        /// 
-        int? GetDefaultLanguageId();
-
-        /// 
-        /// 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);
-
-        /// 
-        /// 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();
-    }
+    ///     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 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 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 identifier.
+    /// 
+    /// 
+    ///     This can be optimized and bypass all deep cloning.
+    /// 
+    int? GetDefaultLanguageId();
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     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();
 }
diff --git a/src/Umbraco.Core/Services/ILocalizedTextService.cs b/src/Umbraco.Core/Services/ILocalizedTextService.cs
index c49a4e6b2f0b..23e3888ea0b3 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
-{
-    // 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.
+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
+{
     /// 
-    /// The entry point to localize any key in the text storage source for a given culture
+    ///     Localize a key with variables
     /// 
-    /// 
-    /// 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
-    {
-        /// 
-        /// Localize a key with variables
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This can be null
-        /// 
-        string Localize(string? area, string? alias, CultureInfo? culture, IDictionary? tokens = null);
-
+    /// 
+    /// 
+    /// 
+    /// 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> GetAllStoredValuesByAreaAndAlias(CultureInfo culture);
 
-        /// 
-        /// Returns all key/values in storage for the given culture
-        /// 
-        /// 
-        IDictionary GetAllStoredValues(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();
+    /// 
+    ///     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);
-    }
+    /// 
+    ///     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);
 }
diff --git a/src/Umbraco.Core/Services/IMacroService.cs b/src/Umbraco.Core/Services/IMacroService.cs
index a75547dd6d83..141b278d93c2 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
+    /// 
+    /// Alias to retrieve an  for
+    /// An  object
+    IMacro? GetByAlias(string alias);
+
+    IEnumerable GetAll();
+
+    IEnumerable GetAll(params int[] ids);
+
+    IEnumerable GetAll(params Guid[] ids);
+
+    IMacro? GetById(int 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);
+
+    /// 
+    ///     Saves an 
     /// 
-    public interface IMacroService : IService
-    {
-
-        /// 
-        /// Gets an  object by its alias
-        /// 
-        /// Alias to retrieve an  for
-        /// An  object
-        IMacro? GetByAlias(string alias);
-
-        IEnumerable GetAll();
-
-        IEnumerable GetAll(params int[] ids);
-
-        IEnumerable GetAll(params Guid[] ids);
-
-        IMacro? GetById(int 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);
-
-        /// 
-        /// 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 an  by its alias
-        ///// 
-        ///// Alias to retrieve an  for
-        ///// An  object
-        //IMacroPropertyType GetMacroPropertyTypeByAlias(string alias);
-    }
+    ///  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 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 6e72777bfa42..508168b877ab 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 fe14bdda0f29..86440b1119d3 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 
-    /// 
-    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);
-
-        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, 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
-        /// 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
-        /// 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 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 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 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 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 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  objects, which reside at the first level / root
-        /// 
-        /// An Enumerable list of  objects
-        IEnumerable GetRootMedia();
-
-        /// 
-        /// 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);
-
-        /// 
-        /// 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);
-
-        /// 
-        /// 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);
-
-        /// 
-        /// 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 media in the recycle bin
-        /// 
-        bool RecycleBinSmells();
-
-        /// 
-        /// 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);
-
-        /// 
-        /// 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);
-
-        /// 
-        /// 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 single  object
-        /// 
-        /// The  to save
-        /// Id of the User saving the Media
-        Attempt Save(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);
-
-        /// 
-        /// Gets an  object by its 'UniqueId'
-        /// 
-        /// Guid key of the Media to retrieve
-        /// 
-        IMedia? GetById(Guid key);
-
-        /// 
-        /// Gets a collection of  objects by Level
-        /// 
-        /// The level to retrieve Media from
-        /// An Enumerable list of  objects
-        IEnumerable? GetByLevel(int level);
-
-        /// 
-        /// Gets a specific version of an  item.
-        /// 
-        /// Id of the version to retrieve
-        /// An  item
-        IMedia? GetVersion(int versionId);
-
-        /// 
-        /// Gets a collection of an  objects versions by Id
-        /// 
-        /// 
-        /// An Enumerable list of  objects
-        IEnumerable GetVersions(int id);
-
-        /// 
-        /// 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 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);
-
-        /// 
-        /// 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 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.
-        /// 
-        /// Id of the  to retrieve ancestors for
-        /// An Enumerable list of  objects
-        IEnumerable GetAncestors(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.
-        /// 
-        /// Id of the  to retrieve the parent from
-        /// Parent  object
-        IMedia? GetParent(int id);
-
-        /// 
-        /// Gets the parent of the current media as an  item.
-        /// 
-        ///  to retrieve the parent from
-        /// Parent  object
-        IMedia? GetParent(IMedia media);
-
-        /// 
-        /// 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
-        /// 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);
-
-        /// 
-        /// 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);
-
-        /// 
-        /// Gets the content of a media as a stream.
-        /// 
-        /// The filesystem path to the media.
-        /// The content of the media.
-        Stream GetMediaFileContentStream(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);
-    }
+    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.
+    /// 
+    /// 
+    ///     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
+    /// 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
+    /// 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 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 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 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 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 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  objects, which reside at the first level / root
+    /// 
+    /// An Enumerable list of  objects
+    IEnumerable GetRootMedia();
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     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 media in the recycle bin
+    /// 
+    bool RecycleBinSmells();
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     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 single  object
+    /// 
+    /// The  to save
+    /// Id of the User saving the Media
+    Attempt Save(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);
+
+    /// 
+    ///     Gets an  object by its 'UniqueId'
+    /// 
+    /// Guid key of the Media to retrieve
+    /// 
+    ///     
+    /// 
+    IMedia? GetById(Guid key);
+
+    /// 
+    ///     Gets a collection of  objects by Level
+    /// 
+    /// The level to retrieve Media from
+    /// An Enumerable list of  objects
+    IEnumerable? GetByLevel(int level);
+
+    /// 
+    ///     Gets a specific version of an  item.
+    /// 
+    /// Id of the version to retrieve
+    /// An  item
+    IMedia? GetVersion(int versionId);
+
+    /// 
+    ///     Gets a collection of an  objects versions by Id
+    /// 
+    /// 
+    /// An Enumerable list of  objects
+    IEnumerable GetVersions(int id);
+
+    /// 
+    ///     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 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);
+
+    /// 
+    ///     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 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.
+    /// 
+    /// Id of the  to retrieve ancestors for
+    /// An Enumerable list of  objects
+    IEnumerable GetAncestors(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.
+    /// 
+    /// Id of the  to retrieve the parent from
+    /// Parent  object
+    IMedia? GetParent(int id);
+
+    /// 
+    ///     Gets the parent of the current media as an  item.
+    /// 
+    ///  to retrieve the parent from
+    /// Parent  object
+    IMedia? GetParent(IMedia media);
+
+    /// 
+    ///     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
+    /// 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);
+
+    /// 
+    ///     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);
+
+    /// 
+    ///     Gets the content of a media as a stream.
+    /// 
+    /// The filesystem path to the media.
+    /// The content of the media.
+    Stream GetMediaFileContentStream(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);
 }
diff --git a/src/Umbraco.Core/Services/IMediaTypeService.cs b/src/Umbraco.Core/Services/IMediaTypeService.cs
index e00d86613b6a..a00b9ae5c657 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 9b8c4a8d536e..24cc6845adf8 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 d6e0480091a0..ec600efab7a0 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.
-    /// 
-    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 = "");
-
-        /// 
-        /// 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
-        /// 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
-        /// 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);
-
-        /// 
-        /// 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 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 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 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);
-
-        /// 
-        /// 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, 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
-        /// The type of match to make as . Default is 
-        /// 
-        IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact);
-    }
+    ///     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 = "");
+
+    /// 
+    ///     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
+    /// 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
+    /// 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);
+
+    /// 
+    ///     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 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 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 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);
+
+    /// 
+    ///     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, 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
+    /// 
+    ///     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 4a52438d5ef1..6a70e620a165 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 94dbbf3da99e..dc96535f8b88 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
+{
+    /// 
+    ///     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
 {
     /// 
-    /// Defines part of the MemberService, which is specific to methods used by the membership provider.
+    ///     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);
+
+    /// 
+    ///     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);
 
     /// 
-    /// 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.
+    ///     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);
-
-        /// 
-        /// 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
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 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);
-    }
+    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);
 }
diff --git a/src/Umbraco.Core/Services/IMembershipRoleService.cs b/src/Umbraco.Core/Services/IMembershipRoleService.cs
index 5c62a84973f9..538ae4fb8c8d 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
-{
-    public interface IMembershipRoleService
-        where T : class, IMembershipUser
-    {
-        void AddRole(string roleName);
+namespace Umbraco.Cms.Core.Services;
 
-        IEnumerable GetAllRoles();
+public interface IMembershipRoleService
+    where T : class, IMembershipUser
+{
+    void AddRole(string roleName);
 
-        IEnumerable GetAllRoles(int memberId);
+    IEnumerable GetAllRoles();
 
-        IEnumerable GetAllRoles(string username);
+    IEnumerable GetAllRoles(int memberId);
 
-        IEnumerable GetAllRolesIds();
+    IEnumerable GetAllRoles(string username);
 
-        IEnumerable GetAllRolesIds(int memberId);
+    IEnumerable GetAllRolesIds();
 
-        IEnumerable GetAllRolesIds(string username);
+    IEnumerable GetAllRolesIds(int memberId);
 
-        IEnumerable GetMembersInRole(string roleName);
+    IEnumerable GetAllRolesIds(string username);
 
-        IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
+    IEnumerable GetMembersInRole(string roleName);
 
-        bool DeleteRole(string roleName, bool throwIfBeingUsed);
+    IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
 
-        void AssignRole(string username, string roleName);
+    bool DeleteRole(string roleName, bool throwIfBeingUsed);
 
-        void AssignRoles(string[] usernames, string[] roleNames);
+    void AssignRole(string username, string roleName);
 
-        void DissociateRole(string username, string roleName);
+    void AssignRoles(string[] usernames, string[] roleNames);
 
-        void DissociateRoles(string[] usernames, string[] roleNames);
+    void DissociateRole(string username, string roleName);
 
-        void AssignRole(int memberId, string roleName);
+    void DissociateRoles(string[] usernames, string[] roleNames);
 
-        void AssignRoles(int[] memberIds, string[] roleNames);
+    void AssignRole(int memberId, string roleName);
 
-        void DissociateRole(int memberId, string roleName);
+    void AssignRoles(int[] memberIds, string[] roleNames);
 
-        void DissociateRoles(int[] memberIds, string[] roleNames);
+    void DissociateRole(int memberId, string roleName);
 
-        void ReplaceRoles(string[] usernames, string[] roleNames);
+    void DissociateRoles(int[] memberIds, string[] roleNames);
 
-        void ReplaceRoles(int[] memberIds, string[] roleNames);
+    void ReplaceRoles(string[] usernames, 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 a2aca2821e7d..7a8dc2023f0e 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 e55cfd71d031..72f3ebe87361 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 50d91c1512e0..d442a7199f45 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 cf65b1aa67f9..8472333d1920 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 842989835442..40f39628be26 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);
-
-        /// 
-        /// 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);
-
-        /// 
-        /// Returns the advertised installed packages
-        /// 
-        /// 
-        IEnumerable GetAllInstalledPackages();
-
-        InstalledPackage? GetInstalledPackageByName(string packageName);
-
-        /// 
-        /// Returns the created packages
-        /// 
-        /// 
-        IEnumerable GetAllCreatedPackages();
-
-        /// 
-        /// Returns a created package by id
-        /// 
-        /// 
-        /// 
-        PackageDefinition? GetCreatedPackageById(int id);
-
-        void DeleteCreatedPackage(int id, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Persists a package definition to storage
-        /// 
-        /// 
-        bool SaveCreatedPackage(PackageDefinition definition);
-
-        /// 
-        /// Creates the package file and returns it's physical path
-        /// 
-        /// 
-        string ExportCreatedPackage(PackageDefinition definition);
-
-    }
+    /// 
+    ///     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);
+
+    InstallationSummary InstallCompiledPackageData(XDocument? packageXml, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Returns the advertised installed packages
+    /// 
+    /// 
+    IEnumerable GetAllInstalledPackages();
+
+    InstalledPackage? GetInstalledPackageByName(string packageName);
+
+    /// 
+    ///     Returns the created packages
+    /// 
+    /// 
+    IEnumerable GetAllCreatedPackages();
+
+    /// 
+    ///     Returns a created package by id
+    /// 
+    /// 
+    /// 
+    PackageDefinition? GetCreatedPackageById(int id);
+
+    void DeleteCreatedPackage(int id, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Persists a package definition to storage
+    /// 
+    /// 
+    bool SaveCreatedPackage(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 c2b8824340ae..e854d0f7f521 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 96d8ca5d1ba4..fb4f080e0348 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
-{
-    public interface IPublicAccessService : IService
-    {
-
-        /// 
-        /// Gets all defined entries and associated rules
-        /// 
-        /// 
-        IEnumerable GetAll();
+namespace Umbraco.Cms.Core.Services;
 
-        /// 
-        /// Gets the entry defined for the content item's path
-        /// 
-        /// 
-        /// Returns null if no entry is found
-        PublicAccessEntry? GetEntryForContent(IContent content);
+public interface IPublicAccessService : IService
+{
+    /// 
+    ///     Gets all defined entries and associated rules
+    /// 
+    /// 
+    IEnumerable GetAll();
 
-        /// 
-        /// 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's path
+    /// 
+    /// 
+    /// Returns null if no entry is found
+    PublicAccessEntry? GetEntryForContent(IContent content);
 
-        /// 
-        /// Returns true if the content has an entry for it's path
-        /// 
-        /// 
-        /// 
-        Attempt IsProtected(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);
 
-        /// 
-        /// 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 for it's path
+    /// 
+    /// 
+    /// 
+    Attempt IsProtected(IContent content);
 
-        /// 
-        /// Adds a rule if the entry doesn't already exist
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        Attempt?> AddRule(IContent content, string ruleType, string ruleValue);
+    /// 
+    ///     Returns true if the content has an entry based on a content path
+    /// 
+    /// 
+    /// 
+    Attempt IsProtected(string contentPath);
 
-        /// 
-        /// Removes a rule
-        /// 
-        /// 
-        /// 
-        /// 
-        Attempt RemoveRule(IContent content, string ruleType, string ruleValue);
+    /// 
+    ///     Adds a rule if the entry doesn't already exist
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    Attempt?> AddRule(IContent content, string ruleType, string ruleValue);
 
-        /// 
-        /// Saves the entry
-        /// 
-        /// 
-        Attempt Save(PublicAccessEntry entry);
+    /// 
+    ///     Removes a rule
+    /// 
+    /// 
+    /// 
+    /// 
+    Attempt RemoveRule(IContent content, string ruleType, string ruleValue);
 
-        /// 
-        /// Deletes the entry and all associated rules
-        /// 
-        /// 
-        Attempt Delete(PublicAccessEntry entry);
+    /// 
+    ///     Saves the entry
+    /// 
+    /// 
+    Attempt Save(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 3c061db4660b..9da327a60065 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 a0825611f73f..6f8fa9b75a54 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
-        IRelationType? GetRelationTypeById(int 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 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 '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 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 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 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 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 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);
-
-        /// 
-        /// 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 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 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 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 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);
-
-        /// 
-        /// 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);
-
-        /// 
-        /// 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 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 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 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
-        /// 
-        /// 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
-        /// 
-        /// 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);
-
-        void Save(IEnumerable relations);
-
-        /// 
-        /// Saves a 
-        /// 
-        /// RelationType to Save
-        void Save(IRelationType relationType);
-
-        /// 
-        /// Deletes a 
-        /// 
-        /// Relation to Delete
-        void Delete(IRelation relation);
-
-        /// 
-        /// 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);
-
-
-
-    }
+    /// 
+    ///     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(Guid id);
+
+    /// 
+    ///     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 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
+    /// 
+    /// 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
+    /// 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
+    /// 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
+    /// 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
+    /// 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);
+
+    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 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 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 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 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 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 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);
+
+    /// 
+    ///     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 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);
+
+    /// 
+    ///     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 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
+    /// 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);
+
+    /// 
+    ///     Saves a 
+    /// 
+    /// Relation to save
+    void Save(IRelation relation);
+
+    void Save(IEnumerable relations);
+
+    /// 
+    ///     Saves a 
+    /// 
+    /// RelationType to Save
+    void Save(IRelationType relationType);
+
+    /// 
+    ///     Deletes a 
+    /// 
+    /// Relation to Delete
+    void Delete(IRelation relation);
+
+    /// 
+    ///     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);
 }
diff --git a/src/Umbraco.Core/Services/IRuntime.cs b/src/Umbraco.Core/Services/IRuntime.cs
index caa430ce1f7e..53ac51f5851c 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 3c765a07480a..a57667101099 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 ded733963b8b..515896cafceb 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 e469de9a061f..4a084920792b 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 6ca00a8dbee3..3147b34a5662 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 70c4ba81b6eb..5e2f164a351e 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;
-
-namespace Umbraco.Cms.Core.Services
+using Umbraco.Cms.Core.Models;
+
+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
-    /// 
-    /// 
-    /// 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);
-
-        /// 
-        /// 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 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 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 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 document tags.
-        /// 
-        IEnumerable GetAllContentTags(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 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 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 a tagged entity.
+    /// 
+    TaggedEntity? GetTaggedEntityById(int id);
+
+    /// 
+    ///     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 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 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 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 document tags.
+    /// 
+    IEnumerable GetAllContentTags(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 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 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);
 }
diff --git a/src/Umbraco.Core/Services/ITrackedReferencesService.cs b/src/Umbraco.Core/Services/ITrackedReferencesService.cs
index dea99c0f6d1a..16b953c35ac0 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 b67e36e15b7e..d61fca066a9b 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 30b221742c96..d0509a9283e0 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);
-    }
+    /// 
+    ///     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);
+[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);
-    }
+    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 2e0f2a5f17e5..2f1e65f00aa2 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 c6b2c6870292..1d4caaa5268e 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 e63ee3f6973c..0bb1d10cc4d9 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 9a63fcf0adb1..40a3fbd89978 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 00acb7ad043b..7aa746ae27a8 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();
-
-        private readonly Dictionary> _id2Key = new Dictionary>();
-        private readonly Dictionary> _key2Id = new Dictionary>();
-
-        public IdKeyMap(ICoreScopeProvider scopeProvider, IIdKeyMapRepository idKeyMapRepository)
-        {
-            _scopeProvider = scopeProvider;
-            _idKeyMapRepository = 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);
-        }
+    private bool _disposedValue;
 
-        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 @@ private void PopulateLocked()
             {
                 // 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 @@ private Attempt PopulateAndGetKeyForId(int id, UmbracoObjectTypes umbracoO
         }
 #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 @@ public Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType)
                 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
-
-            int? val = null;
+        // 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;
 
-            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();
+        if (val == null)
+        {
+            using (ICoreScope scope = _scopeProvider.CreateCoreScope())
+            {
+                val = _idKeyMapRepository.GetIdForKey(key, umbracoObjectType);
+                scope.Complete();
+            }
+        }
 
-            // cache reservations, when something is saved this cache is cleared anyways
-            //if (umbracoObjectType == UmbracoObjectTypes.IdReservation)
-            //    Attempt.Succeed(val.Value);
+        if (val == null)
+        {
+            return Attempt.Fail();
+        }
 
-            try
+        // 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.EnterWriteLock();
-                _id2Key[val.Value] = new TypedId(key, umbracoObjectType);
-                _key2Id[key] = new TypedId(val.Value, umbracoObjectType);
+                _locker.ExitWriteLock();
             }
-            finally
+        }
+
+        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)
             {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
+                _id2Key[id] = new TypedId(key, umbracoObjectType);
+                _key2Id[key] = new TypedId(id, umbracoObjectType);
             }
-
-            return Attempt.Succeed(val.Value);
         }
-
-        public Attempt GetIdForUdi(Udi udi)
+        finally
         {
-            var guidUdi = udi as GuidUdi;
-            if (guidUdi == null)
-                return Attempt.Fail();
-
-            var umbracoType = UdiEntityTypeHelper.ToUmbracoObjectType(guidUdi.EntityType);
-            return GetIdForKey(guidUdi.Guid, umbracoType);
+            if (_locker.IsWriteLockHeld)
+            {
+                _locker.ExitWriteLock();
+            }
         }
+    }
 
-        public Attempt GetUdiForId(int id, UmbracoObjectTypes umbracoObjectType)
+    public Attempt GetIdForUdi(Udi udi)
+    {
+        var guidUdi = udi as GuidUdi;
+        if (guidUdi == 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)
-        {
-            bool empty;
+        UmbracoObjectTypes umbracoType = UdiEntityTypeHelper.ToUmbracoObjectType(guidUdi.EntityType);
+        return GetIdForKey(guidUdi.Guid, umbracoType);
+    }
 
-            try
+    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)
             {
-                _locker.EnterReadLock();
-                if (_id2Key.TryGetValue(id, out var key) && key.UmbracoObjectType == umbracoObjectType) return Attempt.Succeed(key.Id);
-                empty = _id2Key.Count == 0;
+                return Attempt.Succeed(key.Id);
             }
-            finally
+
+            empty = _id2Key.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,
@@ -237,133 +259,156 @@ public Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType)
                 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
-
-            Guid? val = null;
+        // 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;
 
-            if (_dictionary.TryGetValue(umbracoObjectType, out var mappers))
-                if ((val = mappers.id2key(id)) == default(Guid)) val = null;
-
-            if (val == null)
+        if (_dictionary.TryGetValue(umbracoObjectType, out (Func id2key, Func key2id) mappers))
+        {
+            if ((val = mappers.id2key(id)) == default(Guid))
             {
-                using (var scope = _scopeProvider.CreateCoreScope())
-                {
-                    val = _idKeyMapRepository.GetIdForKey(id, 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[id] = new TypedId(val.Value, umbracoObjectType);
-                _key2Id[val.Value] = new TypedId(id, umbracoObjectType);
-            }
-            finally
+        if (val == null)
+        {
+            using (ICoreScope scope = _scopeProvider.CreateCoreScope())
             {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
+                val = _idKeyMapRepository.GetIdForKey(id, umbracoObjectType);
+                scope.Complete();
             }
+        }
 
-            return Attempt.Succeed(val.Value);
+        if (val == null)
+        {
+            return Attempt.Fail();
         }
 
-        // invoked on UnpublishedPageCacheRefresher.RefreshAll
-        // anything else will use the id-specific overloads
-        public void ClearCache()
+        // cache reservations, when something is saved this cache is cleared anyways
+        // if (umbracoObjectType == UmbracoObjectTypes.IdReservation)
+        //    Attempt.Succeed(val.Value);
+        try
         {
-            try
-            {
-                _locker.EnterWriteLock();
-                _id2Key.Clear();
-                _key2Id.Clear();
-            }
-            finally
+            _locker.EnterWriteLock();
+            _id2Key[id] = new TypedId(val.Value, umbracoObjectType);
+            _key2Id[val.Value] = new TypedId(id, umbracoObjectType);
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
             {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
+                _locker.ExitWriteLock();
             }
         }
 
-        public void ClearCache(int id)
+        return Attempt.Succeed(val.Value);
+    }
+
+    // invoked on UnpublishedPageCacheRefresher.RefreshAll
+    // anything else will use the id-specific overloads
+    public void ClearCache()
+    {
+        try
         {
-            try
-            {
-                _locker.EnterWriteLock();
-                if (_id2Key.TryGetValue(id, out var key) == false) return;
-                _id2Key.Remove(id);
-                _key2Id.Remove(key.Id);
-            }
-            finally
+            _locker.EnterWriteLock();
+            _id2Key.Clear();
+            _key2Id.Clear();
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
             {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
+                _locker.ExitWriteLock();
             }
         }
+    }
 
-        public void ClearCache(Guid key)
+    public void ClearCache(int id)
+    {
+        try
         {
-            try
+            _locker.EnterWriteLock();
+            if (_id2Key.TryGetValue(id, out TypedId key) == false)
             {
-                _locker.EnterWriteLock();
-                if (_key2Id.TryGetValue(key, out var id) == false) return;
-                _id2Key.Remove(id.Id);
-                _key2Id.Remove(key);
+                return;
             }
-            finally
+
+            _id2Key.Remove(id);
+            _key2Id.Remove(key.Id);
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
             {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
+                _locker.ExitWriteLock();
             }
         }
+    }
 
-        // ReSharper disable ClassNeverInstantiated.Local
-        // ReSharper disable UnusedAutoPropertyAccessor.Local
-        private class TypedIdDto
+    public void ClearCache(Guid key)
+    {
+        try
         {
-            public int Id { get; set; }
-            public Guid UniqueId { get; set; }
-            public Guid NodeObjectType { get; set; }
-        }
-        // ReSharper restore ClassNeverInstantiated.Local
-        // ReSharper restore UnusedAutoPropertyAccessor.Local
+            _locker.EnterWriteLock();
+            if (_key2Id.TryGetValue(key, out TypedId id) == false)
+            {
+                return;
+            }
 
-        private struct TypedId
+            _id2Key.Remove(id.Id);
+            _key2Id.Remove(key);
+        }
+        finally
         {
-            public TypedId(T id, UmbracoObjectTypes umbracoObjectType)
+            if (_locker.IsWriteLockHeld)
             {
-                UmbracoObjectType = umbracoObjectType;
-                Id = id;
+                _locker.ExitWriteLock();
             }
-
-            public UmbracoObjectTypes UmbracoObjectType { get; }
-
-            public T Id { get; }
         }
+    }
 
-        protected virtual void Dispose(bool disposing)
+    protected virtual void Dispose(bool disposing)
+    {
+        if (!_disposedValue)
         {
-            if (!_disposedValue)
+            if (disposing)
             {
-                if (disposing)
-                {
-                    _locker.Dispose();
-                }
-                _disposedValue = true;
+                _locker.Dispose();
             }
+
+            _disposedValue = true;
         }
+    }
 
-        public void Dispose()
+    // ReSharper restore ClassNeverInstantiated.Local
+    // ReSharper restore UnusedAutoPropertyAccessor.Local
+    private struct TypedId
+    {
+        public TypedId(T id, UmbracoObjectTypes umbracoObjectType)
         {
-            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
-            Dispose(disposing: true);
+            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 eb1632be8aef..00bd00aa91e4 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 834c0d311659..0a38e3c28469 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;
+            return _repository.Get(key)?.Value;
         }
+    }
 
-        /// 
-        public string? GetValue(string key)
+    /// 
+    public IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix)
+    {
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _repository.Get(key)?.Value;
-            }
+            return _repository.FindByKeyPrefix(keyPrefix);
         }
+    }
 
-        /// 
-        public IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix)
+    /// 
+    public void SetValue(string key, string value)
+    {
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope())
         {
-            using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true))
+            scope.WriteLock(Constants.Locks.KeyValues);
+
+            IKeyValue? keyValue = _repository.Get(key);
+            if (keyValue == null)
             {
-                return _repository.FindByKeyPrefix(keyPrefix);
+                keyValue = new KeyValue { Identifier = key, Value = value, UpdateDate = DateTime.Now };
             }
-        }
-
-        /// 
-        public void SetValue(string key, string value)
-        {
-            using (var scope = _scopeProvider.CreateCoreScope())
+            else
             {
-                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;
-                }
+                keyValue.Value = value;
+                keyValue.UpdateDate = DateTime.Now;
+            }
 
-                _repository.Save(keyValue);
+            _repository.Save(keyValue);
 
-                scope.Complete();
-            }
+            scope.Complete();
         }
+    }
 
-        /// 
-        public void SetValue(string key, string originValue, string newValue)
+    /// 
+    public void SetValue(string key, string originValue, string newValue)
+    {
+        if (!TrySetValue(key, originValue, newValue))
         {
-            if (!TrySetValue(key, originValue, newValue))
-                throw new InvalidOperationException("Could not set the value.");
+            throw new InvalidOperationException("Could not set the value.");
         }
+    }
 
-        /// 
-        public bool TrySetValue(string key, string originalValue, string newValue)
+    /// 
+    public bool TrySetValue(string key, string originalValue, string newValue)
+    {
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope())
         {
-            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.WriteLock(Constants.Locks.KeyValues);
 
-                scope.Complete();
+            IKeyValue? keyValue = _repository.Get(key);
+            if (keyValue == null || keyValue.Value != originalValue)
+            {
+                return false;
             }
 
-            return true;
+            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 262697c9352e..3046ddafb516 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
+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 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;
+    }
+
     /// 
-    /// Represents the Localization Service, which is an easy access to operations involving  and 
+    ///     Adds or updates a translation for a dictionary item and language
     /// 
-    internal class LocalizationService : RepositoryService, ILocalizationService
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     This does not save the item, that needs to be done explicitly
+    /// 
+    public void AddOrUpdateDictionaryValue(IDictionaryItem item, ILanguage? language, string value)
     {
-        private readonly IDictionaryRepository _dictionaryRepository;
-        private readonly ILanguageRepository _languageRepository;
-        private readonly IAuditRepository _auditRepository;
-
-        public LocalizationService(
-            ICoreScopeProvider provider,
-            ILoggerFactory loggerFactory,
-            IEventMessagesFactory eventMessagesFactory,
-            IDictionaryRepository dictionaryRepository,
-            IAuditRepository auditRepository,
-            ILanguageRepository languageRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+        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);
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new DictionaryItemSavingNotification(item, eventMessages);
 
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return item;
-                }
-                _dictionaryRepository.Save(item);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return item;
+            }
 
-                // ensure the lazy Language callback is assigned
-                EnsureDictionaryItemLanguageCallback(item);
+            _dictionaryRepository.Save(item);
 
-                scope.Notifications.Publish(new DictionaryItemSavedNotification(item, eventMessages).WithStateFrom(savingNotification));
+            // ensure the lazy Language callback is assigned
+            EnsureDictionaryItemLanguageCallback(item);
 
-                scope.Complete();
+            scope.Notifications.Publish(
+                new DictionaryItemSavedNotification(item, eventMessages).WithStateFrom(savingNotification));
 
-                return item;
-            }
+            scope.Complete();
+
+            return item;
         }
+    }
 
-        /// 
-        /// Gets a  by its  id
-        /// 
-        /// Id of the 
-        /// 
-        public IDictionaryItem? GetDictionaryItemById(int id)
+    /// 
+    ///     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);
+
+            // ensure the lazy Language callback is assigned
+            EnsureDictionaryItemLanguageCallback(item);
+            return item;
         }
+    }
 
-        /// 
-        /// Gets a  by its  id
-        /// 
-        /// Id of the 
-        /// 
-        public IDictionaryItem? GetDictionaryItemById(Guid id)
+    /// 
+    ///     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(id);
-                //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)
+    /// 
+    ///     Gets a  by its key
+    /// 
+    /// Key of the 
+    /// 
+    ///     
+    /// 
+    public IDictionaryItem? GetDictionaryItemByKey(string key)
+    {
+        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(key);
+
+            // ensure the lazy Language callback is assigned
+            EnsureDictionaryItemLanguageCallback(item);
+            return item;
         }
+    }
 
         /// 
         /// Gets a list of children for a 
@@ -189,26 +204,30 @@ public IEnumerable GetDictionaryItemChildren(Guid parentId)
                 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 @@ public IEnumerable GetRootDictionaryItems()
             }
         }
 
-        /// 
-        /// 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);
+                scope.Complete();
+                return;
+            }
 
-                // ensure the lazy Language callback is assigned
-                // ensure the lazy Language callback is assigned
+            _dictionaryRepository.Save(dictionaryItem);
 
-                EnsureDictionaryItemLanguageCallback(dictionaryItem);
-                scope.Notifications.Publish(new DictionaryItemSavedNotification(dictionaryItem, eventMessages).WithStateFrom(savingNotification));
+            // 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();
-            }
+            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;
-                }
+                scope.Complete();
+                return;
+            }
 
-                _dictionaryRepository.Delete(dictionaryItem);
-                scope.Notifications.Publish(new DictionaryItemDeletedNotification(dictionaryItem, eventMessages).WithStateFrom(deletingNotification));
+            _dictionaryRepository.Delete(dictionaryItem);
+            scope.Notifications.Publish(
+                new DictionaryItemDeletedNotification(dictionaryItem, eventMessages)
+                    .WithStateFrom(deletingNotification));
 
-                Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem");
+            Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem");
 
-                scope.Complete();
-            }
+            scope.Complete();
         }
+    }
 
-        /// 
-        /// Gets a  by its id
-        /// 
-        /// Id of the 
-        /// 
-        public ILanguage? GetLanguageById(int id)
+    /// 
+    ///     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.Get(id);
         }
+    }
 
-        /// 
-        /// Gets a  by its iso code
-        /// 
-        /// Iso Code of the language (ie. en-US)
-        /// 
-        public ILanguage? GetLanguageByIsoCode(string? isoCode)
+    /// 
+    ///     Gets a  by its iso code
+    /// 
+    /// Iso Code of the language (ie. en-US)
+    /// 
+    ///     
+    /// 
+    public ILanguage? GetLanguageByIsoCode(string? isoCode)
+    {
+        if (isoCode is null)
         {
-            if (isoCode is null)
-            {
-                return null;
-            }
-            
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetByIsoCode(isoCode);
-            }
+            return null;
         }
 
-        /// 
-        public int? GetLanguageIdByIsoCode(string isoCode)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetIdByIsoCode(isoCode);
-            }
+            return _languageRepository.GetByIsoCode(isoCode);
         }
+    }
 
-        /// 
-        public string? GetLanguageIsoCodeById(int id)
+    /// 
+    public int? GetLanguageIdByIsoCode(string isoCode)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetIsoCodeById(id);
-            }
+            return _languageRepository.GetIdByIsoCode(isoCode);
         }
+    }
 
-        /// 
-        public string GetDefaultLanguageIsoCode()
+    /// 
+    public string? GetLanguageIsoCodeById(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetDefaultIsoCode();
-            }
+            return _languageRepository.GetIsoCodeById(id);
         }
+    }
 
-        /// 
-        public int? GetDefaultLanguageId()
+    /// 
+    public string GetDefaultLanguageIsoCode()
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetDefaultId();
-            }
+            return _languageRepository.GetDefaultIsoCode();
         }
+    }
 
-        /// 
-        /// Gets all available languages
-        /// 
-        /// An enumerable list of  objects
-        public IEnumerable GetAllLanguages()
+    /// 
+    public int? GetDefaultLanguageId()
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetMany();
-            }
+            return _languageRepository.GetDefaultId();
+        }
+    }
+
+    /// 
+    ///     Gets all available languages
+    /// 
+    /// An enumerable list of  objects
+    public IEnumerable GetAllLanguages()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _languageRepository.GetMany();
         }
+    }
 
-        /// 
-        /// 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)
+    /// 
+    ///     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())
-            {
-                // write-lock languages to guard against race conds when dealing with default language
-                scope.WriteLock(Cms.Core.Constants.Locks.Languages);
+            // write-lock languages to guard against race conds when dealing with default language
+            scope.WriteLock(Constants.Locks.Languages);
 
-                // look for cycles - within write-lock
-                if (language.FallbackLanguageId.HasValue)
+            // 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;
             }
-        }
 
-        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;
+            _languageRepository.Save(language);
+            scope.Notifications.Publish(
+                new LanguageSavedNotification(language, eventMessages).WithStateFrom(savingNotification));
 
-            var id = language.FallbackLanguageId;
-            while (true) // assuming languages does not already contains a cycle, this must end
-            {
-                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
-            }
+            Audit(AuditType.Save, "Save Language", userId, language.Id, UmbracoObjectTypes.Language.GetName());
+
+            scope.Complete();
         }
+    }
 
-        /// 
-        /// 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)
+    /// 
+    ///     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())
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
+            // write-lock languages to guard against race conds when dealing with default language
+            scope.WriteLock(Constants.Locks.Languages);
+
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingLanguageNotification = new LanguageDeletingNotification(language, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingLanguageNotification))
             {
-                // write-lock languages to guard against race conds when dealing with default language
-                scope.WriteLock(Cms.Core.Constants.Locks.Languages);
+                scope.Complete();
+                return;
+            }
 
-                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);
 
-                // 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));
 
-                scope.Notifications.Publish(new LanguageDeletedNotification(language, eventMessages).WithStateFrom(deletingLanguageNotification));
+            Audit(AuditType.Delete, "Delete Language", userId, language.Id, UmbracoObjectTypes.Language.GetName());
+            scope.Complete();
+        }
+    }
 
-                Audit(AuditType.Delete, "Delete Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language));
-                scope.Complete();
-            }
+    public Dictionary GetDictionaryItemKeyMap()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _dictionaryRepository.GetDictionaryItemKeyMap();
         }
+    }
 
-        private void Audit(AuditType type, string message, int userId, int objectId, string? entityType)
+    private bool CreatesCycle(ILanguage language, IDictionary languages)
+    {
+        // a new language is not referenced yet, so cannot be part of a cycle
+        if (!language.HasIdentity)
         {
-            _auditRepository.Save(new AuditItem(objectId, type, userId, entityType, message));
+            return false;
         }
 
-        /// 
-        /// 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)
+        var id = language.FallbackLanguageId;
+
+        // assuming languages does not already contains a cycle, this must end
+        while (true)
         {
-            var item = d as DictionaryItem;
-            if (item == null) return;
+            if (!id.HasValue)
+            {
+                return false; // no fallback means no cycle
+            }
 
-            item.GetLanguage = GetLanguageById;
-            var translations = item.Translations?.OfType();
-            if (translations is not null)
+            if (id.Value == language.Id)
             {
-                foreach (var trans in translations)
-                    trans.GetLanguage = GetLanguageById;
+                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 6aa7e8fb2b85..839e52f49e15 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;
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+        if (fileSources == null)
+        {
+            throw new ArgumentNullException(nameof(fileSources));
+        }
 
-        private IDictionary>>> _dictionarySource =>
-            _dictionarySourceLazy.Value;
+        _dictionarySourceLazy =
+            new Lazy>>>>(() =>
+                FileSourcesToAreaDictionarySources(fileSources.Value));
+        _noAreaDictionarySourceLazy =
+            new Lazy>>>(() =>
+                FileSourcesToNoAreaDictionarySources(fileSources.Value));
+        _fileSources = fileSources;
+    }
 
-        private IDictionary>> _noAreaDictionarySource =>
-            _noAreaDictionarySourceLazy.Value;
+    /// 
+    ///     Initializes with an XML source
+    /// 
+    /// 
+    /// 
+    public LocalizedTextService(
+        IDictionary> source,
+        ILogger logger)
+    {
+        if (source == null)
+        {
+            throw new ArgumentNullException(nameof(source));
+        }
 
-        private readonly Lazy>>>>
-            _dictionarySourceLazy;
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 
-        private readonly Lazy>>> _noAreaDictionarySourceLazy;
+        _dictionarySourceLazy =
+            new Lazy>>>>(() =>
+                XmlSourcesToAreaDictionary(source));
+        _noAreaDictionarySourceLazy =
+            new Lazy>>>(() =>
+                XmlSourceToNoAreaDictionary(source));
+    }
 
-        /// 
-        /// Initializes with a file sources instance
-        /// 
-        /// 
-        /// 
-        public LocalizedTextService(Lazy fileSources,
-            ILogger logger)
-        {
-            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;
-        }
+    [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)
+    {
+    }
 
-        private IDictionary>> FileSourcesToNoAreaDictionarySources(
-            LocalizedTextServiceFileSources fileSources)
+    /// 
+    ///     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 xmlSources = fileSources.GetXmlSources();
+            Dictionary> areaAliaValue =
+                GetAreaStoredTranslations(source, cultureDictionary.Key);
 
-            return XmlSourceToNoAreaDictionary(xmlSources);
+            cultureNoAreaDictionary.Add(
+                cultureDictionary.Key,
+                new Lazy>(() => GetAliasValues(areaAliaValue)));
         }
 
-        private IDictionary>> XmlSourceToNoAreaDictionary(
-            IDictionary> xmlSources)
-        {
-            var cultureNoAreaDictionary = new Dictionary>>();
-            foreach (var xmlSource in xmlSources)
-            {
-                var noAreaAliasValue =
-                    new Lazy>(() => GetNoAreaStoredTranslations(xmlSources, xmlSource.Key));
-                cultureNoAreaDictionary.Add(xmlSource.Key, noAreaAliasValue);
-            }
+        _noAreaDictionarySourceLazy =
+            new Lazy>>>(() => cultureNoAreaDictionary);
+    }
 
-            return cultureNoAreaDictionary;
-        }
+    private IDictionary>>> DictionarySource =>
+        _dictionarySourceLazy.Value;
 
-        private IDictionary>>>
-            FileSourcesToAreaDictionarySources(LocalizedTextServiceFileSources fileSources)
-        {
-            var xmlSources = fileSources.GetXmlSources();
-            return XmlSourcesToAreaDictionary(xmlSources);
-        }
+    private IDictionary>> NoAreaDictionarySource =>
+        _noAreaDictionarySourceLazy.Value;
 
-        private IDictionary>>>
-            XmlSourcesToAreaDictionary(IDictionary> xmlSources)
+    public string Localize(string? area, string? alias, CultureInfo? culture, IDictionary? tokens = null)
+    {
+        if (culture == null)
         {
-            var cultureDictionary =
-                new Dictionary>>>();
-            foreach (var xmlSource in xmlSources)
-            {
-                var areaAliaValue =
-                    new Lazy>>(() =>
-                        GetAreaStoredTranslations(xmlSources, xmlSource.Key));
-                cultureDictionary.Add(xmlSource.Key, areaAliaValue);
-            }
-
-            return cultureDictionary;
+            throw new ArgumentNullException(nameof(culture));
         }
 
-        /// 
-        /// Initializes with an XML source
-        /// 
-        /// 
-        /// 
-        public LocalizedTextService(IDictionary> source,
-            ILogger logger)
+        // This is what the legacy ui service did
+        if (string.IsNullOrEmpty(alias))
         {
-            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));
+            return string.Empty;
         }
 
-        [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)
-        {
+        // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
+        culture = ConvertToSupportedCultureWithRegionCode(culture);
 
-        }
-        /// 
-        /// Initializes with a source of a dictionary of culture -> areas -> sub dictionary of keys/values
-        /// 
-        /// 
-        /// 
-        public LocalizedTextService(
-            IDictionary>>> source,
-            ILogger logger)
+        return GetFromDictionarySource(culture, area, alias, tokens);
+    }
+
+    /// 
+    ///     Returns all key/values in storage for the given culture
+    /// 
+    public IDictionary GetAllStoredValues(CultureInfo culture)
+    {
+        if (culture == null)
         {
-            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);
+            throw new ArgumentNullException(nameof(culture));
+        }
 
-                cultureNoAreaDictionary.Add(cultureDictionary.Key,
-                    new Lazy>(() => GetAliasValues(areaAliaValue)));
-            }
+        // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
+        culture = ConvertToSupportedCultureWithRegionCode(culture);
 
-            _noAreaDictionarySourceLazy =
-                new Lazy>>>(() => cultureNoAreaDictionary);
+        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);
         }
 
-        private static Dictionary GetAliasValues(
-            Dictionary> areaAliaValue)
+        IDictionary result = new Dictionary();
+
+        // convert all areas + keys to a single key with a '/'
+        foreach (KeyValuePair> area in DictionarySource[culture].Value)
         {
-            var aliasValue = new Dictionary();
-            foreach (var area in areaAliaValue)
+            foreach (KeyValuePair key in area.Value)
             {
-                foreach (var alias 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)
                 {
-                    if (!aliasValue.ContainsKey(alias.Key))
-                    {
-                        aliasValue.Add(alias.Key, alias.Value);
-                    }
+                    result.Add(dictionaryKey, key.Value);
                 }
             }
+        }
+
+        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)
+        {
+            throw new ArgumentNullException("currentCulture");
+        }
 
-            return aliasValue;
+        if (_fileSources == null)
+        {
+            return currentCulture;
         }
 
-        public string Localize(string key, CultureInfo culture, IDictionary? tokens = null)
+        if (currentCulture.Name.Length > 2)
         {
-            if (culture == null) throw new ArgumentNullException(nameof(culture));
+            return currentCulture;
+        }
 
-            //This is what the legacy ui service did
-            if (string.IsNullOrEmpty(key))
-                return string.Empty;
+        Attempt attempt =
+            _fileSources.Value.TryConvert2LetterCultureTo4Letter(currentCulture.TwoLetterISOLanguageName);
+        return attempt.Success ? attempt.Result! : currentCulture;
+    }
 
-            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);
+    /// 
+    ///     Returns all key/values in storage for the given culture
+    /// 
+    /// 
+    public IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture)
+    {
+        if (culture == null)
+        {
+            throw new ArgumentNullException("culture");
         }
 
-        public string Localize(string? area, string? alias, CultureInfo? culture,
-            IDictionary? tokens = null)
+        // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
+        culture = ConvertToSupportedCultureWithRegionCode(culture);
+
+        if (DictionarySource.ContainsKey(culture) == false)
         {
-            if (culture == null) throw new ArgumentNullException(nameof(culture));
+            _logger.LogWarning(
+                "The culture specified {Culture} was not found in any configured sources for this service",
+                culture);
+            return new Dictionary>(0);
+        }
 
-            //This is what the legacy ui service did
-            if (string.IsNullOrEmpty(alias))
-                return string.Empty;
+        return DictionarySource[culture].Value;
+    }
 
-            // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
-            culture = ConvertToSupportedCultureWithRegionCode(culture);
+    public string Localize(string key, CultureInfo culture, IDictionary? tokens = null)
+    {
+        if (culture == null)
+        {
+            throw new ArgumentNullException(nameof(culture));
+        }
 
-            return GetFromDictionarySource(culture, area, alias, tokens);
+        // This is what the legacy ui service did
+        if (string.IsNullOrEmpty(key))
+        {
+            return string.Empty;
         }
 
-        /// 
-        /// Returns all key/values in storage for the given culture
-        /// 
-        public IDictionary GetAllStoredValues(CultureInfo culture)
+        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)
         {
-            if (culture == null) throw new ArgumentNullException(nameof(culture));
+            return value;
+        }
 
-            // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
-            culture = ConvertToSupportedCultureWithRegionCode(culture);
+        foreach (KeyValuePair token in tokens)
+        {
+            value = value.Replace(string.Concat("%", token.Key, "%"), token.Value);
+        }
 
-            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 value;
+    }
 
-            IDictionary result = new Dictionary();
-            //convert all areas + keys to a single key with a '/'
-            foreach (var area in _dictionarySource[culture].Value)
+    private static Dictionary GetAliasValues(
+        Dictionary> areaAliaValue)
+    {
+        var aliasValue = new Dictionary();
+        foreach (KeyValuePair> area in areaAliaValue)
+        {
+            foreach (KeyValuePair alias in area.Value)
             {
-                foreach (var key in area.Value)
+                if (!aliasValue.ContainsKey(alias.Key))
                 {
-                    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);
-                    }
+                    aliasValue.Add(alias.Key, alias.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);
-                }
+        return aliasValue;
+    }
 
-                overallResult.Add(area.Attribute("alias")!.Value, result);
-            }
+    private IDictionary>> FileSourcesToNoAreaDictionarySources(
+        LocalizedTextServiceFileSources fileSources)
+    {
+        IDictionary> xmlSources = fileSources.GetXmlSources();
 
-            //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];
-                    }
+        return XmlSourceToNoAreaDictionary(xmlSources);
+    }
 
-                    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);
-                    }
+    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);
+        }
 
-                    if (!overallResult.ContainsKey(area.Attribute("alias")!.Value))
-                    {
-                        overallResult.Add(area.Attribute("alias")!.Value, result);
-                    }
-                }
-            }
+        return cultureNoAreaDictionary;
+    }
+
+    private IDictionary>>>
+        FileSourcesToAreaDictionarySources(LocalizedTextServiceFileSources fileSources)
+    {
+        IDictionary> xmlSources = fileSources.GetXmlSources();
+        return XmlSourcesToAreaDictionary(xmlSources);
+    }
 
-            return overallResult;
+    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);
         }
 
-        private Dictionary GetNoAreaStoredTranslations(
-            IDictionary> xmlSource, CultureInfo cult)
+        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);
-            var keys = xmlSource[cult].Value.XPathSelectElements("//key");
-
-            foreach (var key in keys)
+            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
+
+                // 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))
+            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)
             {
-                var keysEn = xmlSource[englishCulture].Value.XPathSelectElements("//key");
+                IDictionary
+                    result = new Dictionary(StringComparer.InvariantCulture);
+                if (overallResult.ContainsKey(area.Attribute("alias")!.Value))
+                {
+                    result = overallResult[area.Attribute("alias")!.Value];
+                }
 
-                foreach (var key in keys)
+                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
+
+                    // 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);
-            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)
+                if (!overallResult.ContainsKey(area.Attribute("alias")!.Value))
                 {
-                    //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.Attribute("alias")!.Value, result);
                 }
-
-                overallResult.Add(area.Key, result);
             }
-
-            return overallResult;
-        }
-
-        /// 
-        /// Returns a list of all currently supported cultures
-        /// 
-        /// 
-        public IEnumerable GetSupportedCultures()
-        {
-            return _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) throw new ArgumentNullException("currentCulture");
-
-            if (_fileSources == null) return currentCulture;
-            if (currentCulture.Name.Length > 2) return currentCulture;
+        return overallResult;
+    }
 
-            var attempt = _fileSources.Value.TryConvert2LetterCultureTo4Letter(currentCulture.TwoLetterISOLanguageName);
-            return attempt.Success ? attempt.Result! : currentCulture;
-        }
+    private Dictionary GetNoAreaStoredTranslations(
+        IDictionary> xmlSource, CultureInfo cult)
+    {
+        var result = new Dictionary(StringComparer.InvariantCulture);
+        IEnumerable keys = xmlSource[cult].Value.XPathSelectElements("//key");
 
-        private string GetFromDictionarySource(CultureInfo culture, string? area, string key,
-            IDictionary? tokens)
+        foreach (XElement key in keys)
         {
-            if (_dictionarySource.ContainsKey(culture) == false)
+            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)
             {
-                _logger.LogWarning(
-                    "The culture specified {Culture} was not found in any configured sources for this service",
-                    culture);
-                return "[" + key + "]";
+                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");
 
-            string? found = null;
-            if (string.IsNullOrWhiteSpace(area))
-            {
-                _noAreaDictionarySource[culture].Value.TryGetValue(key, out found);
-            }
-            else
+            foreach (XElement key in keys)
             {
-                if (_dictionarySource[culture].Value.TryGetValue(area, out var areaDictionary))
-                {
-                    areaDictionary.TryGetValue(key, out found);
-                }
+                var dictionaryKey =
+                    (string)key.Attribute("alias")!;
 
-                if (found == null)
+                // 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)
                 {
-                    _noAreaDictionarySource[culture].Value.TryGetValue(key, out found);
+                    result.Add(dictionaryKey, key.Value);
                 }
             }
+        }
 
+        return result;
+    }
 
-            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 + "]";
-        }
+    private Dictionary> GetAreaStoredTranslations(
+        IDictionary>>> dictionarySource,
+        CultureInfo cult)
+    {
+        var overallResult = new Dictionary>(StringComparer.InvariantCulture);
+        Lazy>> areaDict = dictionarySource[cult];
 
-        /// 
-        /// 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)
+        foreach (KeyValuePair> area in areaDict.Value)
         {
-            if (tokens == null || tokens.Any() == false)
+            var result = new Dictionary(StringComparer.InvariantCulture);
+            ICollection keys = area.Value.Keys;
+            foreach (var key in keys)
             {
-                return value;
+                // 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]);
+                }
             }
 
-            foreach (var token in tokens)
-            {
-                value = value.Replace(string.Concat("%", token.Key, "%"), token.Value);
-            }
+            overallResult.Add(area.Key, result);
+        }
 
-            return value;
+        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 + "]";
         }
 
-        /// 
-        /// Returns all key/values in storage for the given culture
-        /// 
-        /// 
-        public IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture)
+        string? found = null;
+        if (string.IsNullOrWhiteSpace(area))
         {
-            if (culture == null)
+            NoAreaDictionarySource[culture].Value.TryGetValue(key, out found);
+        }
+        else
+        {
+            if (DictionarySource[culture].Value.TryGetValue(area, out IDictionary? areaDictionary))
             {
-                throw new ArgumentNullException("culture");
+                areaDictionary.TryGetValue(key, out found);
             }
 
-            // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
-            culture = ConvertToSupportedCultureWithRegionCode(culture);
-
-            if (_dictionarySource.ContainsKey(culture) == false)
+            if (found == null)
             {
-                _logger.LogWarning(
-                    "The culture specified {Culture} was not found in any configured sources for this service",
-                    culture);
-                return new Dictionary>(0);
+                NoAreaDictionarySource[culture].Value.TryGetValue(key, out found);
             }
+        }
 
-            return _dictionarySource[culture].Value;
+        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 8a9559f7bca8..f8b44759a023 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);
+
+    /// 
+    ///     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));
+
     /// 
-    /// Extension methods for ILocalizedTextService
+    ///     Localize a key without any variables
     /// 
-    public static class LocalizedTextServiceExtensions
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    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);
-
-        public static string Localize(this ILocalizedTextService manager, string? area, string? alias)
-            => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture);
-
-        /// 
-        /// 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));
-
-        /// 
-        /// 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));
-
-         /// 
-         /// 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;
-
-             return variables.Select((s, i) => new { index = i.ToString(CultureInfo.InvariantCulture), value = s })
-                 .ToDictionary(keyvals => keyvals.index, keyvals => keyvals.value);
-         }
-
-         public static string? UmbracoDictionaryTranslate(this ILocalizedTextService manager, ICultureDictionary cultureDictionary, string? text)
-         {
-             if (text == null)
-                 return null;
-
-             if (text.StartsWith("#") == false)
-                 return text;
-
-             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 (text == null)
+        {
+            return null;
+        }
+
+        if (text.StartsWith("#") == false)
+        {
+            return text;
+        }
+
+        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;
+    }
+
+    /// 
+    ///     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;
+        }
 
+        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 41d12f9a4563..26a2e9fb60ca 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,290 +7,312 @@
 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
 {
+    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())
+    {
+    }
+
     /// 
-    /// Exposes the XDocument sources from files for the default localization text service and ensure caching is taken care of
+    ///     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 class LocalizedTextServiceFileSources
+    public LocalizedTextServiceFileSources(
+        ILogger logger,
+        AppCaches appCaches,
+        DirectoryInfo fileSourceFolder,
+        IEnumerable supplementFileSources,
+        IDirectoryContents directoryContents)
     {
-        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())
+        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
-            )
-        {
-            if (logger == null) throw new ArgumentNullException("logger");
-            if (appCaches == null) throw new ArgumentNullException("cache");
-            if (fileSourceFolder == null) throw new ArgumentNullException("fileSourceFolder");
-
-            _logger = logger;
-            _directoryContents = directoryContents;
-            _cache = appCaches.RuntimeCache;
-            _fileSourceFolder = fileSourceFolder;
-            _supplementFileSources = supplementFileSources;
-
-            //Create the lazy source for the _xmlSources
-            _xmlSources = new Lazy>>(() =>
-            {
-                var result = new Dictionary>();
+        _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>>(() =>
+        {
+            var result = new Dictionary>();
 
-                var files = GetLanguageFiles();
+            IEnumerable files = GetLanguageFiles();
 
-                if (!files.Any())
-                {
-                    return result;
-                }
+            if (!files.Any())
+            {
+                return result;
+            }
 
-                foreach (var fileInfo in files)
+            foreach (IFileInfo fileInfo in files)
+            {
+                IFileInfo 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)
                 {
-                    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
+                                    {
+                                        culture = CultureInfo.GetCultureInfo(cultureVal);
+
+                                        // add to the tracked dictionary
+                                        _twoLetterCultureConverter[filename] = culture;
+                                    }
+                                    catch (CultureNotFoundException)
                                     {
-                                        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.
-                                        }
+                                        _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)
+                }
+
+                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),
+                        () =>
                     {
-                        culture = CultureInfo.GetCultureInfo(filename);
-                    }
+                        XDocument xdoc;
 
-                    //get the lazy value from cache
-                    result[culture] = new Lazy(() => _cache.GetCacheItem(
-                        string.Format("{0}-{1}", typeof(LocalizedTextServiceFileSources).Name, culture.Name), () =>
+                        // load in primary
+                        using (Stream fs = localCopy.CreateReadStream())
                         {
-                            XDocument xdoc;
-
-                            //load in primary
-                            using (var fs = localCopy.CreateReadStream())
-                            {
-                                xdoc = XDocument.Load(fs);
-                            }
+                            xdoc = XDocument.Load(fs);
+                        }
 
-                            //load in supplementary
-                            MergeSupplementaryFiles(culture, xdoc);
+                        // load in supplementary
+                        MergeSupplementaryFiles(culture, xdoc);
 
-                            return xdoc;
-                        }, isSliding: true, timeout: TimeSpan.FromMinutes(10))!);
-                }
-                return result;
-            });
+                        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())
+    {
+    }
 
-        private IEnumerable GetLanguageFiles()
-        {
-            var result = new List();
+    /// 
+    ///     returns all xml sources for all culture files found in the folder
+    /// 
+    /// 
+    public IDictionary> GetXmlSources() => _xmlSources.Value;
 
-            if (_fileSourceFolder is not null && _fileSourceFolder.Exists)
-            {
+    private IEnumerable GetLanguageFiles()
+    {
+        var result = new List();
 
-                result.AddRange(
-                    new PhysicalDirectoryContents(_fileSourceFolder.FullName)
-                    .Where(x => !x.IsDirectory && x.Name.EndsWith(".xml"))
-                );
-            }
+        if (_fileSourceFolder is not null && _fileSourceFolder.Exists)
+        {
+            result.AddRange(
+                new PhysicalDirectoryContents(_fileSourceFolder.FullName)
+                    .Where(x => !x.IsDirectory && x.Name.EndsWith(".xml")));
+        }
 
-            if (_directoryContents.Exists)
-            {
-                result.AddRange(
+        if (_directoryContents.Exists)
+        {
+            result.AddRange(
                 _directoryContents
-                        .Where(x => !x.IsDirectory && x.Name.EndsWith(".xml"))
-                );
-            }
-
-            return result;
+                    .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()
+        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)
         {
-            return _xmlSources.Value;
+            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 TryConvert2LetterCultureTo4Letter(string twoLetterCulture)
-        {
-            if (twoLetterCulture.Length != 2) return Attempt.Fail();
+        // This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
+        Dictionary> resolved = _xmlSources.Value;
 
-            //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 _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)
+        {
+            throw new ArgumentNullException("culture");
         }
 
-        // 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) throw new ArgumentNullException("culture");
+        // This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
+        Dictionary> resolved = _xmlSources.Value;
 
-            //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();
+    }
 
-            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)
+        {
+            return;
         }
 
-        private void MergeSupplementaryFiles(CultureInfo culture, XDocument xMasterDoc)
+        if (_supplementFileSources != null)
         {
-            if (xMasterDoc.Root == 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)
-                    );
-                });
-
-                foreach (var supplementaryFile in found)
+                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 (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)
+                        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 (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)
                         {
-                            _logger.LogError(ex, "Could not load file into XML {File}", supplementaryFile.File.FullName);
-                            continue;
+                            // add the whole thing
+                            xMasterDoc.Root.Add(xArea);
                         }
-
-                        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))
+                        else
                         {
-                            var areaAlias = (string)xArea.Attribute("alias")!;
-
-                            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);
-                            }
+                            MergeChildKeys(xArea, areaFound, supplementaryFile.OverwriteCoreKeys);
                         }
                     }
                 }
             }
         }
+    }
+
+    private void MergeChildKeys(XElement source, XElement destination, bool overwrite)
+    {
+        if (destination == null)
+        {
+            throw new ArgumentNullException("destination");
+        }
 
-        private void MergeChildKeys(XElement source, XElement destination, bool overwrite)
+        if (source == null)
         {
-            if (destination == null) throw new ArgumentNullException("destination");
-            if (source == null) throw new ArgumentNullException("source");
+            throw new ArgumentNullException("source");
+        }
 
-            //merge in the child elements
-            foreach (var key in source.Elements("key")
-                .Where(x => ((string)x.Attribute("alias")!).IsNullOrWhiteSpace() == false))
+        // 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)
             {
-                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;
-                }
+                // 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 7fe5e0e48a99..cff9a55234b9 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)
     {
+        File = file ?? throw new ArgumentNullException("file");
+        OverwriteCoreKeys = overwriteCoreKeys;
+    }
 
-        public LocalizedTextServiceSupplementaryFileSource(FileInfo file, bool overwriteCoreKeys)
-        {
-            if (file == null) throw new ArgumentNullException("file");
-
-            File = file;
-            OverwriteCoreKeys = overwriteCoreKeys;
-        }
+    public FileInfo File { get; }
 
-        public FileInfo File { get; private set; }
-        public bool OverwriteCoreKeys { get; private set; }
-    }
+    public bool OverwriteCoreKeys { get; }
 }
diff --git a/src/Umbraco.Core/Services/MacroService.cs b/src/Umbraco.Core/Services/MacroService.cs
index 6b598921e1b8..73889895e257 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.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
 {
+    private readonly IAuditRepository _auditRepository;
+    private readonly IMacroRepository _macroRepository;
+
+    public MacroService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IMacroRepository macroRepository,
+        IAuditRepository auditRepository)
+        : base(provider, loggerFactory, eventMessagesFactory)
+    {
+        _macroRepository = macroRepository;
+        _auditRepository = auditRepository;
+    }
+
     /// 
-    /// Represents the Macro Service, which is an easy access to operations involving 
+    ///     Gets an  object by its alias
     /// 
-    internal class MacroService : RepositoryService, IMacroWithAliasService
+    /// Alias to retrieve an  for
+    /// An  object
+    public IMacro? GetByAlias(string alias)
     {
-        private readonly IMacroRepository _macroRepository;
-        private readonly IAuditRepository _auditRepository;
-
-        public MacroService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMacroRepository macroRepository, IAuditRepository auditRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+        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);
-            }
+            return macroWithAliasRepository.GetByAlias(alias);
         }
+    }
+
+    public IEnumerable GetAll() => GetAll(new int[0]);
 
-        public IEnumerable GetAll()
+    public IEnumerable GetAll(params int[] ids)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            return GetAll(new int[0]);
+            return _macroRepository.GetMany(ids);
         }
+    }
 
-        public IEnumerable GetAll(params int[] ids)
+    public IEnumerable GetAll(params Guid[] ids)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _macroRepository.GetMany(ids);
-            }
+            return _macroRepository.GetMany(ids);
         }
+    }
 
-        public IEnumerable GetAll(params Guid[] ids)
+    public IEnumerable GetAll(params string[] aliases)
+    {
+        if (_macroRepository is not IMacroWithAliasRepository macroWithAliasRepository)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _macroRepository.GetMany(ids);
-            }
+            var hashset = new HashSet(aliases);
+            return GetAll().Where(x => hashset.Contains(x.Alias));
         }
 
-        public IEnumerable GetAll(params string[] aliases)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            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();
-            }
+            return macroWithAliasRepository.GetAllByAlias(aliases);
         }
+    }
 
-        public IMacro? GetById(int id)
+    public IMacro? GetById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _macroRepository.Get(id);
-            }
+            return _macroRepository.Get(id);
         }
+    }
 
-        public IMacro? GetById(Guid id)
+    public IMacro? GetById(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _macroRepository.Get(id);
-            }
+            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)
+    /// 
+    ///     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())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingNotification = new MacroDeletingNotification(macro, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new MacroDeletingNotification(macro, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
+                scope.Complete();
+                return;
+            }
 
-                _macroRepository.Delete(macro);
+            _macroRepository.Delete(macro);
 
-                scope.Notifications.Publish(new MacroDeletedNotification(macro, eventMessages).WithStateFrom(deletingNotification));
-                Audit(AuditType.Delete, userId, -1);
+            scope.Notifications.Publish(
+                new MacroDeletedNotification(macro, eventMessages).WithStateFrom(deletingNotification));
+            Audit(AuditType.Delete, userId, -1);
 
-                scope.Complete();
-            }
+            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)
+    /// 
+    ///     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())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new MacroSavingNotification(macro, eventMessages);
-
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new MacroSavingNotification(macro, eventMessages);
 
-                if (string.IsNullOrWhiteSpace(macro.Name))
-                {
-                    throw new ArgumentException("Cannot save macro with empty name.");
-                }
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return;
+            }
 
-                _macroRepository.Save(macro);
+            if (string.IsNullOrWhiteSpace(macro.Name))
+            {
+                throw new ArgumentException("Cannot save macro with empty name.");
+            }
 
-                scope.Notifications.Publish(new MacroSavedNotification(macro, eventMessages).WithStateFrom(savingNotification));
-                Audit(AuditType.Save, userId, -1);
+            _macroRepository.Save(macro);
 
-                scope.Complete();
-            }
-        }
+            scope.Notifications.Publish(
+                new MacroSavedNotification(macro, eventMessages).WithStateFrom(savingNotification));
+            Audit(AuditType.Save, userId, -1);
 
-        ///// 
-        ///// 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"));
+            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 13ba415feef7..325677407e5c 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 @@ public class MediaService : RepositoryService, IMediaService
 
         #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 @@ public MediaService(ICoreScopeProvider provider, MediaFileManager mediaFileManag
 
         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 @@ public int CountDescendants(int parentId, string? mediaTypeAlias = null)
         /// 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 @@ public IMedia CreateMedia(string name, Guid parentId, string mediaTypeAlias, int
         /// 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 @@ public IMedia CreateMedia(string? name, int parentId, string mediaTypeAlias, int
         /// 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 @@ public IMedia CreateMedia(string name, string mediaTypeAlias, int userId = Cms.C
         /// 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
+                throw new ArgumentNullException(nameof(parent));
+            }
 
-                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.");
-                }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            // not locking since not saving anything
 
-                var media = new Core.Models.Media(name, parent, mediaType);
-                CreateMedia(scope, media, parent, userId, false);
+            IMediaType? mediaType = GetMediaType(mediaTypeAlias);
+            if (mediaType == null)
+            {
+                throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback
+            }
 
-                scope.Complete();
-                return media;
+            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 @@ public IMedia CreateMedia(string name, IMedia? parent, string mediaTypeAlias, in
         /// 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())
-            {
-                // locking the media tree secures media types too
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            // locking the media tree secures media types too
+            scope.WriteLock(Constants.Locks.MediaTree);
 
-                var mediaType = GetMediaType(mediaTypeAlias); // + locks
-                if (mediaType == null)
-                    throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback
+            IMediaType? 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
+            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
+            }
 
-                var media = parentId > 0 ? new Core.Models.Media(name, parent, mediaType) : new Core.Models.Media(name, parentId, mediaType);
-                CreateMedia(scope, media, parent, userId, true);
+            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;
-            }
+            scope.Complete();
+            return media;
         }
 
         /// 
@@ -267,25 +283,28 @@ public IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTyp
         /// 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
+                throw new ArgumentNullException(nameof(parent));
+            }
 
-                var media = new Core.Models.Media(name, parent, mediaType);
-                CreateMedia(scope, media, parent, userId, true);
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            // locking the media tree secures media types too
+            scope.WriteLock(Constants.Locks.MediaTree);
 
-                scope.Complete();
-                return media;
+            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 @@ private void CreateMedia(ICoreScope scope, Core.Models.Media media, IMedia? pare
             }
 
             if (withIdentity == false)
+            {
                 return;
+            }
 
             Audit(AuditType.New, media.CreatorId, media.Id, $"Media '{media.Name}' was created with Id {media.Id}");
         }
@@ -325,11 +346,9 @@ private void CreateMedia(ICoreScope scope, Core.Models.Media media, IMedia? pare
         /// 
         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 @@ private void CreateMedia(ICoreScope scope, Core.Models.Media media, IMedia? pare
         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 @@ public IEnumerable GetByIds(IEnumerable ids)
         /// 
         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 @@ public IEnumerable GetByIds(IEnumerable ids)
         /// 
         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 (ordering == null)
-                ordering = Ordering.By("sortOrder");
+            if (pageSize <= 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageSize));
+            }
 
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            if (ordering == null)
             {
-                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 (ordering == null)
-                ordering = Ordering.By("sortOrder");
+            if (pageSize <= 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageSize));
+            }
 
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            if (ordering == null)
             {
-                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 @@ public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex,
         /// 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 @@ public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex,
         /// 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 @@ public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex,
         /// 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 @@ public IEnumerable GetVersions(int id)
         public IEnumerable GetAncestors(int id)
         {
             // intentionally not locking
-            var media = GetById(id);
+            IMedia? media = GetById(id);
             return GetAncestors(media);
         }
 
@@ -480,82 +504,105 @@ public IEnumerable GetAncestors(int id)
         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);
+                return new List();
             }
+
+            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);
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
 
-                var query = Query()?.Where(x => x.ParentId == id);
-                return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
-            }
+            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);
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
 
-                //if the id is System Root, then just get all
-                if (id != Cms.Core.Constants.System.Root)
+            //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)
                 {
-                    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);
+                    totalChildren = 0;
+                    return Enumerable.Empty();
                 }
-                return GetPagedLocked(GetPagedDescendantQuery(null), pageIndex, pageSize, out totalChildren, filter, ordering);
+
+                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 @@ private IEnumerable GetPagedLocked(IQuery? query, long pageIndex
         public IMedia? GetParent(int id)
         {
             // intentionally not locking
-            var media = GetById(id);
+            IMedia? media = GetById(id);
             return GetParent(media);
         }
 
@@ -580,8 +627,10 @@ private IEnumerable GetPagedLocked(IQuery? query, long pageIndex
         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 @@ private IEnumerable GetPagedLocked(IQuery? query, long pageIndex
         /// 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 @@ public IEnumerable GetPagedMediaInRecycleBin(long pageIndex, int pageSiz
         /// 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 @@ public bool HasChildren(int id)
         /// 
         /// 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 @@ public bool HasChildren(int id)
 
                 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 @@ public bool HasChildren(int id)
         /// 
         /// 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 @@ public bool HasChildren(int id)
 
                 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 @@ public bool HasChildren(int id)
                 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 @@ public bool HasChildren(int id)
         /// 
         /// 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 @@ public bool HasChildren(int id)
                     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 @@ void DoDelete(IMedia c)
             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 @@ void DoDelete(IMedia c)
         /// 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 @@ private void DeleteVersions(ICoreScope scope, bool wlock, int id, DateTime versi
             }
 
             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 @@ private void DeleteVersions(ICoreScope scope, bool wlock, int id, DateTime versi
         /// 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;
-                }
+                scope.Complete();
+                return;
+            }
 
-                if (deletePriorVersions)
-                {
-                    var media = GetVersion(versionId);
-                    if (media is not null)
-                    {
-                        DeleteVersions(scope, true, id, media.UpdateDate, userId);
-                    }
-                }
-                else
+            if (deletePriorVersions)
+            {
+                IMedia? media = GetVersion(versionId);
+                if (media is not null)
                 {
-                    scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                    DeleteVersions(scope, true, id, media.UpdateDate, userId);
                 }
+            }
+            else
+            {
+                scope.WriteLock(Constants.Locks.MediaTree);
+            }
 
-                _mediaRepository.DeleteVersion(versionId);
+            _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.Notifications.Publish(new MediaDeletedVersionsNotification(id, evtMsgs, specificVersion: versionId).WithStateFrom(deletingVersionsNotification));
+            Audit(AuditType.Delete, userId, Constants.System.Root, "Delete Media by version");
 
-                scope.Complete();
-            }
+            scope.Complete();
         }
 
         #endregion
@@ -887,21 +933,21 @@ public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int u
         /// 
         /// 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 @@ public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int u
                     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 @@ public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int u
         /// 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 @@ public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int u
 
             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 @@ private void PerformMoveLocked(IMedia media, int parentId, IMedia? parent, int u
             //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 @@ private void PerformMoveMediaLocked(IMedia media, int userId, bool? trash)
         /// 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 @@ public OperationResult EmptyRecycleBin(int userId = Cms.Core.Constants.Security.
                 }
                 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 @@ public OperationResult EmptyRecycleBin(int userId = Cms.Core.Constants.Security.
 
         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 @@ public bool RecycleBinSmells()
         /// 
         /// 
         /// 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 @@ public bool Sort(IEnumerable items, int userId = Cms.Core.Constants.Secu
 
                 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 @@ public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportO
         {
             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 @@ public long GetMediaFileSize(string filepath)
         /// 
         /// 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 @@ public void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = Cms.C
 
             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 @@ public void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = Cms.C
                         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 @@ public void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = Cms.C
                 }
                 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 @@ public void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = Cms.C
         /// 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));
+            if (mediaTypeAlias == null)
+            {
+                throw new ArgumentNullException(nameof(mediaTypeAlias));
+            }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            if (string.IsNullOrWhiteSpace(mediaTypeAlias))
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTypes);
+                throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(mediaTypeAlias));
+            }
 
-                var query = Query().Where(x => x.Alias == mediaTypeAlias);
-                var mediaType = _mediaTypeRepository.Get(query)?.FirstOrDefault();
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.ReadLock(Constants.Locks.MediaTypes);
 
-                if (mediaType == null)
-                    throw new InvalidOperationException($"No media type matched the specified alias '{mediaTypeAlias}'.");
+            IQuery query = Query().Where(x => x.Alias == mediaTypeAlias);
+            IMediaType? mediaType = _mediaTypeRepository.Get(query)?.FirstOrDefault();
 
-                scope.Complete();
-                return mediaType;
+            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 1cf648c35d26..8d45367e6181 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 6873fb4a3970..eff6ba0fbacc 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.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;
-        }
+    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 };
 
-        // 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 };
+    protected override int[] WriteLockIds { get; } = { Constants.Locks.MediaTree, Constants.Locks.MediaTypes };
 
-        private IMediaService MediaService { get; }
+    protected override Guid ContainedObjectType => Constants.ObjectTypes.MediaType;
 
-        protected override Guid ContainedObjectType => Cms.Core.Constants.ObjectTypes.MediaType;
+    private IMediaService MediaService { get; }
 
-        #region Notifications
+    protected override void DeleteItemsOfTypes(IEnumerable typeIds)
+    {
+        foreach (var typeId in typeIds)
+        {
+            MediaService.DeleteMediaOfType(typeId);
+        }
+    }
 
-        protected override SavingNotification GetSavingNotification(IMediaType item,
-            EventMessages eventMessages) => new MediaTypeSavingNotification(item, eventMessages);
+    #region Notifications
 
-        protected override SavingNotification GetSavingNotification(IEnumerable items,
-            EventMessages eventMessages) => new MediaTypeSavingNotification(items, eventMessages);
+    protected override SavingNotification GetSavingNotification(
+        IMediaType item,
+        EventMessages eventMessages) => new MediaTypeSavingNotification(item, eventMessages);
 
-        protected override SavedNotification GetSavedNotification(IMediaType item,
-            EventMessages eventMessages) => new MediaTypeSavedNotification(item, eventMessages);
+    protected override SavingNotification GetSavingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MediaTypeSavingNotification(items, eventMessages);
 
-        protected override SavedNotification GetSavedNotification(IEnumerable items,
-            EventMessages eventMessages) => new MediaTypeSavedNotification(items, eventMessages);
+    protected override SavedNotification GetSavedNotification(
+        IMediaType item,
+        EventMessages eventMessages) => new MediaTypeSavedNotification(item, eventMessages);
 
-        protected override DeletingNotification GetDeletingNotification(IMediaType item,
-            EventMessages eventMessages) => new MediaTypeDeletingNotification(item, eventMessages);
+    protected override SavedNotification GetSavedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MediaTypeSavedNotification(items, eventMessages);
 
-        protected override DeletingNotification GetDeletingNotification(IEnumerable items,
-            EventMessages eventMessages) => new MediaTypeDeletingNotification(items, eventMessages);
+    protected override DeletingNotification GetDeletingNotification(
+        IMediaType item,
+        EventMessages eventMessages) => new MediaTypeDeletingNotification(item, eventMessages);
 
-        protected override DeletedNotification GetDeletedNotification(IEnumerable items,
-            EventMessages eventMessages) => new MediaTypeDeletedNotification(items, eventMessages);
+    protected override DeletingNotification GetDeletingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MediaTypeDeletingNotification(items, eventMessages);
 
-        protected override MovingNotification GetMovingNotification(MoveEventInfo moveInfo,
-            EventMessages eventMessages) => new MediaTypeMovingNotification(moveInfo, eventMessages);
+    protected override DeletedNotification GetDeletedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MediaTypeDeletedNotification(items, eventMessages);
 
-        protected override MovedNotification GetMovedNotification(
-            IEnumerable> moveInfo, EventMessages eventMessages) =>
-            new MediaTypeMovedNotification(moveInfo, eventMessages);
+    protected override MovingNotification GetMovingNotification(
+        MoveEventInfo moveInfo,
+        EventMessages eventMessages) => new MediaTypeMovingNotification(moveInfo, eventMessages);
 
-        protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new MediaTypeChangedNotification(changes, eventMessages);
+    protected override MovedNotification GetMovedNotification(
+        IEnumerable> moveInfo, EventMessages eventMessages) =>
+        new MediaTypeMovedNotification(moveInfo, eventMessages);
 
-        protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new MediaTypeRefreshedNotification(changes, eventMessages);
+    protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new MediaTypeChangedNotification(changes, eventMessages);
 
-        #endregion
+    protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new MediaTypeRefreshedNotification(changes, eventMessages);
 
-        protected override void DeleteItemsOfTypes(IEnumerable typeIds)
-        {
-            foreach (var typeId in typeIds)
-                MediaService.DeleteMediaOfType(typeId);
-        }
-    }
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/MemberGroupService.cs b/src/Umbraco.Core/Services/MemberGroupService.cs
index 2290f9d84a82..5a68236455a9 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.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;
+    private readonly IMemberGroupRepository _memberGroupRepository;
 
-        public MemberGroupService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IMemberGroupRepository memberGroupRepository)
-            : base(provider, loggerFactory, eventMessagesFactory) =>
-            _memberGroupRepository = memberGroupRepository;
+    public MemberGroupService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMemberGroupRepository memberGroupRepository)
+        : base(provider, loggerFactory, eventMessagesFactory) =>
+        _memberGroupRepository = memberGroupRepository;
 
-        public IEnumerable GetAll()
+    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)
+    public IEnumerable GetByIds(IEnumerable ids)
+    {
+        if (ids == null || ids.Any() == false)
         {
-            if (ids == null || ids.Any() == false)
-            {
-                return new IMemberGroup[0];
-            }
+            return new IMemberGroup[0];
+        }
 
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _memberGroupRepository.GetMany(ids.ToArray());
-            }
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _memberGroupRepository.GetMany(ids.ToArray());
         }
+    }
 
-        public IMemberGroup? GetById(int id)
+    public IMemberGroup? GetById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _memberGroupRepository.Get(id);
-            }
+            return _memberGroupRepository.Get(id);
         }
+    }
 
-        public IMemberGroup? GetById(Guid id)
+    public IMemberGroup? GetById(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _memberGroupRepository.Get(id);
-            }
+            return _memberGroupRepository.Get(id);
         }
+    }
 
-        public IMemberGroup? GetByName(string? name)
+    public IMemberGroup? GetByName(string? name)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _memberGroupRepository.GetByName(name);
-            }
+            return _memberGroupRepository.GetByName(name);
         }
+    }
 
-        public void Save(IMemberGroup memberGroup)
+    public void Save(IMemberGroup memberGroup)
+    {
+        if (string.IsNullOrWhiteSpace(memberGroup.Name))
         {
-            if (string.IsNullOrWhiteSpace(memberGroup.Name))
-            {
-                throw new InvalidOperationException("The name of a MemberGroup can not be empty");
-            }
+            throw new InvalidOperationException("The name of a MemberGroup can not be empty");
+        }
 
-            var evtMsgs = EventMessagesFactory.Get();
+        EventMessages evtMsgs = EventMessagesFactory.Get();
 
-            using (var scope = ScopeProvider.CreateCoreScope())
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var savingNotification = new MemberGroupSavingNotification(memberGroup, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                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)
-        {
-            var evtMsgs = EventMessagesFactory.Get();
+    public void Delete(IMemberGroup memberGroup)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
 
-            using (var scope = ScopeProvider.CreateCoreScope())
+        using (ICoreScope 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 2a4498f7e4a2..76d730dc78a4 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 @@ public class MemberService : RepositoryService, IMemberService
 
         #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 @@ public MemberService(ICoreScopeProvider provider, ILoggerFactory loggerFactory,
         ///  with number of Members for passed in type
         public int GetCount(MemberCountType countType)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-
-                IQuery? query;
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
 
-                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));
-                }
+            IQuery? query;
 
-                return _memberRepository.GetCountByQuery(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);
         }
 
         /// 
@@ -88,11 +90,9 @@ public int GetCount(MemberCountType countType)
         ///  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 @@ public IMember CreateMember(string username, string email, string name, string m
         /// 
         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 @@ IMember IMembershipMemberService.CreateWithIdentity(string username, st
             => 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 @@ public IMember CreateMemberWithIdentity(string username, string email, string na
         }
 
         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 @@ public IMember CreateMemberWithIdentity(string username, string email, IMemberTy
         /// 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 @@ public IMember CreateMemberWithIdentity(string username, string email, string na
         /// 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 @@ public IMember CreateMemberWithIdentity(string username, string email, string na
         /// 
         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);
+                throw new ArgumentNullException(nameof(memberType));
+            }
 
-                // ensure it all still make sense
-                // ensure it all still make sense
-                var vrfy = GetMemberType(scope, memberType.Alias); // + locks
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
 
-                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
-                }
+            // ensure it all still make sense
+            // ensure it all still make sense
+            IMemberType? vrfy = GetMemberType(scope, memberType.Alias); // + locks
 
-                var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType, isApproved, -1);
+            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
+            }
 
-                Save(member);
+            var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType, isApproved, -1);
 
-                scope.Complete();
+            Save(member);
 
-                return member;
-            }
+            scope.Complete();
+
+            return member;
         }
 
         #endregion
@@ -296,11 +300,9 @@ private IMember CreateMemberWithIdentity(string username, string email, string n
         /// 
         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 @@ private IMember CreateMemberWithIdentity(string username, string email, string n
         /// 
         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 @@ private IMember CreateMemberWithIdentity(string username, string email, string n
         /// 
         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"));
-            }
-        }
-
-        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);
+            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, bool orderBySystemField, string? memberTypeAlias, string 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)
         {
-            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 @@ public IEnumerable GetAll(long pageIndex, int pageSize, out long totalR
         /// 
         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 @@ public IEnumerable GetAll(long pageIndex, int pageSize, out long totalR
         /// 
         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 @@ public IEnumerable GetAll(long pageIndex, int pageSize, out long totalR
         /// 
         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 @@ public IEnumerable GetAll(long pageIndex, int pageSize, out long totalR
         /// 
         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 @@ public IEnumerable GetMembersByMemberType(string memberTypeAlias)
         /// 
         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 @@ public IEnumerable GetMembersByMemberType(int memberTypeId)
         /// 
         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 @@ public IEnumerable GetMembersByGroup(string memberGroupName)
         /// 
         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 @@ public IEnumerable GetAllMembers(params int[] ids)
         /// 
         public IEnumerable FindMembersByDisplayName(string displayNameToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query();
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery? 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"));
+            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"));
         }
 
         /// 
@@ -512,34 +509,32 @@ public IEnumerable FindMembersByDisplayName(string displayNameToMatch,
         /// 
         public IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                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));
-                }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery? query = Query();
 
-                return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("Email"));
+            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"));
         }
 
         /// 
@@ -553,34 +548,32 @@ public IEnumerable FindByEmail(string emailStringToMatch, long pageInde
         /// 
         public IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                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));
-                }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery? query = Query();
 
-                return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("LoginName"));
+            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"));
         }
 
         /// 
@@ -592,31 +585,29 @@ public IEnumerable FindByUsername(string login, long pageIndex, int pag
         /// 
         public IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                IQuery query;
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            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);
+            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);
         }
 
         /// 
@@ -628,34 +619,32 @@ public IEnumerable FindByUsername(string login, long pageIndex, int pag
         /// 
         public IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                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));
-                }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query;
 
-                return _memberRepository.Get(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);
         }
 
         /// 
@@ -666,13 +655,11 @@ public IEnumerable FindByUsername(string login, long pageIndex, int pag
         /// 
         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 @@ public IEnumerable FindByUsername(string login, long pageIndex, int pag
         /// 
         public IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                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
-                }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query;
 
-                // TODO: Since this is by property value, we need a GetByPropertyQuery on the repo!
-                return _memberRepository.Get(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);
         }
 
         /// 
@@ -722,11 +707,9 @@ public IEnumerable FindByUsername(string login, long pageIndex, int pag
         /// 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 @@ public bool Exists(int id)
         /// 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 @@ public void Save(IMember member)
             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;
-                }
+                scope.Complete();
+                return;
+            }
 
-                if (string.IsNullOrWhiteSpace(member.Name))
-                {
-                    throw new ArgumentException("Cannot save member with empty name.");
-                }
+            if (string.IsNullOrWhiteSpace(member.Name))
+            {
+                throw new ArgumentException("Cannot save member with empty name.");
+            }
 
-                scope.WriteLock(Constants.Locks.MemberTree);
+            scope.WriteLock(Constants.Locks.MemberTree);
 
-                _memberRepository.Save(member);
+            _memberRepository.Save(member);
 
-                scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
+            scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
 
-                Audit(AuditType.Save, 0, member.Id);
+            Audit(AuditType.Save, 0, member.Id);
 
-                scope.Complete();
-            }
+            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.Complete();
+                return;
+            }
 
-                scope.WriteLock(Constants.Locks.MemberTree);
+            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();
+            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);
-                }
+                _memberRepository.Save(member);
+            }
 
-                scope.Notifications.Publish(new MemberSavedNotification(membersA, evtMsgs).WithStateFrom(savingNotification));
+            scope.Notifications.Publish(new MemberSavedNotification(membersA, evtMsgs).WithStateFrom(savingNotification));
 
-                Audit(AuditType.Save, 0, -1, "Save multiple Members");
+            Audit(AuditType.Save, 0, -1, "Save multiple Members");
 
-                scope.Complete();
-            }
+            scope.Complete();
         }
 
         #endregion
@@ -833,23 +810,21 @@ public void Save(IEnumerable members)
         ///  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 @@ private void DeleteLocked(ICoreScope scope, IMember member, EventMessages evtMsg
 
         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 @@ public void AddRole(string roleName)
 
         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 @@ public IEnumerable GetAllRoles()
         /// 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())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
 
-                if (throwIfBeingUsed)
+            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();
+            IQuery query = Query().Where(g => g.Name == roleName);
+            IMemberGroup[]? found = _memberGroupRepository.Get(query)?.ToArray();
 
-                if (found is not null)
+            if (found is not null)
+            {
+                foreach (IMemberGroup memberGroup in found)
                 {
-                    foreach (IMemberGroup memberGroup in found)
-                    {
-                        _memberGroupService.Delete(memberGroup);
-                    }
+                    _memberGroupService.Delete(memberGroup);
                 }
-
-                scope.Complete();
-                return found?.Length > 0;
             }
+
+            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 @@ public void ReplaceRoles(int[] memberIds, string[] roleNames)
         /// 
         public MemberExportModel? ExportMember(Guid key)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                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))
-                };
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IQuery? query = Query().Where(x => x.Key == key);
+            IMember? member = _memberRepository.Get(query)?.FirstOrDefault();
 
-                scope.Notifications.Publish(new ExportedMemberNotification(member, model));
-
-                return model;
+            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;
         }
 
         private static IEnumerable GetPropertyExportItems(IMember member)
@@ -1156,38 +1097,36 @@ private static IEnumerable GetPropertyExportItems(IMember
         /// 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())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
+            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);
+            // 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();
+            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);
-                }
+            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();
         }
 
         private IMemberType GetMemberType(ICoreScope scope, string memberTypeAlias)
@@ -1226,10 +1165,8 @@ private IMemberType GetMemberType(string memberTypeAlias)
                 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 1d4298984128..67b7f0811181 100644
--- a/src/Umbraco.Core/Services/MemberTypeService.cs
+++ b/src/Umbraco.Core/Services/MemberTypeService.cs
@@ -9,98 +9,145 @@
 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;
+    }
 
-        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 };
 
-        // 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 };
+    protected override int[] WriteLockIds { get; } = { Constants.Locks.MemberTree, Constants.Locks.MemberTypes };
 
-        private IMemberService MemberService { get; }
+    protected override Guid ContainedObjectType => Constants.ObjectTypes.MemberType;
 
-        protected override Guid ContainedObjectType => Cms.Core.Constants.ObjectTypes.MemberType;
+    private IMemberService MemberService { get; }
 
-        #region Notifications
+    public string GetDefault()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(ReadLockIds);
 
-        protected override SavingNotification GetSavingNotification(IMemberType item,
-            EventMessages eventMessages) => new MemberTypeSavingNotification(item, eventMessages);
+            using (IEnumerator e = _memberTypeRepository.GetMany(new int[0]).GetEnumerator())
+            {
+                if (e.MoveNext() == false)
+                {
+                    throw new InvalidOperationException("No member types could be resolved");
+                }
 
-        protected override SavingNotification GetSavingNotification(IEnumerable items,
-            EventMessages eventMessages) => new MemberTypeSavingNotification(items, eventMessages);
+                var first = e.Current.Alias;
+                var current = true;
+                while (e.Current.Alias.InvariantEquals("Member") == false && (current = e.MoveNext()))
+                {
+                }
 
-        protected override SavedNotification GetSavedNotification(IMemberType item,
-            EventMessages eventMessages) => new MemberTypeSavedNotification(item, eventMessages);
+                return current ? e.Current.Alias : first;
+            }
+        }
+    }
 
-        protected override SavedNotification GetSavedNotification(IEnumerable items,
-            EventMessages eventMessages) => new MemberTypeSavedNotification(items, eventMessages);
+    protected override void DeleteItemsOfTypes(IEnumerable typeIds)
+    {
+        foreach (var typeId in typeIds)
+        {
+            MemberService.DeleteMembersOfType(typeId);
+        }
+    }
 
-        protected override DeletingNotification GetDeletingNotification(IMemberType item,
-            EventMessages eventMessages) => new MemberTypeDeletingNotification(item, eventMessages);
+    #region Notifications
 
-        protected override DeletingNotification GetDeletingNotification(IEnumerable items,
-            EventMessages eventMessages) => new MemberTypeDeletingNotification(items, eventMessages);
+    protected override SavingNotification GetSavingNotification(
+        IMemberType item,
+        EventMessages eventMessages) => new MemberTypeSavingNotification(item, eventMessages);
 
-        protected override DeletedNotification GetDeletedNotification(IEnumerable items,
-            EventMessages eventMessages) => new MemberTypeDeletedNotification(items, eventMessages);
+    protected override SavingNotification GetSavingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MemberTypeSavingNotification(items, eventMessages);
 
-        protected override MovingNotification GetMovingNotification(MoveEventInfo moveInfo,
-            EventMessages eventMessages) => new MemberTypeMovingNotification(moveInfo, eventMessages);
+    protected override SavedNotification GetSavedNotification(
+        IMemberType item,
+        EventMessages eventMessages) => new MemberTypeSavedNotification(item, eventMessages);
 
-        protected override MovedNotification GetMovedNotification(
-            IEnumerable> moveInfo, EventMessages eventMessages) =>
-            new MemberTypeMovedNotification(moveInfo, eventMessages);
+    protected override SavedNotification GetSavedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MemberTypeSavedNotification(items, eventMessages);
 
-        protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new MemberTypeChangedNotification(changes, eventMessages);
+    protected override DeletingNotification GetDeletingNotification(
+        IMemberType item,
+        EventMessages eventMessages) => new MemberTypeDeletingNotification(item, eventMessages);
 
-        protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new MemberTypeRefreshedNotification(changes, eventMessages);
+    protected override DeletingNotification GetDeletingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MemberTypeDeletingNotification(items, eventMessages);
 
-        #endregion
+    protected override DeletedNotification GetDeletedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MemberTypeDeletedNotification(items, eventMessages);
 
-        protected override void DeleteItemsOfTypes(IEnumerable typeIds)
-        {
-            foreach (var typeId in typeIds)
-                MemberService.DeleteMembersOfType(typeId);
-        }
+    protected override MovingNotification GetMovingNotification(
+        MoveEventInfo moveInfo,
+        EventMessages eventMessages) => new MemberTypeMovingNotification(moveInfo, eventMessages);
 
-        public string GetDefault()
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
+    protected override MovedNotification GetMovedNotification(
+        IEnumerable> moveInfo, EventMessages eventMessages) =>
+        new MemberTypeMovedNotification(moveInfo, eventMessages);
 
-                using (var e = _memberTypeRepository.GetMany(new int[0]).GetEnumerator())
-                {
-                    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;
-                }
-            }
-        }
-    }
+    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 ca64d42810e5..a5309d35f1f5 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);
+    public MetricsConsentService(
+        IKeyValueService keyValueService,
+        ILogger logger,
+        IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
+        IUserService userService)
+    {
+        _keyValueService = keyValueService;
+        _logger = logger;
+        _backOfficeSecurityAccessor = backOfficeSecurityAccessor;
+        _userService = userService;
+    }
 
-            if (analyticsLevelString is null || Enum.TryParse(analyticsLevelString, out TelemetryLevel analyticsLevel) is false)
-            {
-                return TelemetryLevel.Basic;
-            }
+    public TelemetryLevel GetConsentLevel()
+    {
+        var analyticsLevelString = _keyValueService.GetValue(Key);
 
-            return analyticsLevel;
+        if (analyticsLevelString is null ||
+            Enum.TryParse(analyticsLevelString, out TelemetryLevel analyticsLevel) is false)
+        {
+            return TelemetryLevel.Basic;
         }
 
-        public void SetConsentLevel(TelemetryLevel telemetryLevel)
-        {
-            var currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
-            if (currentUser is null)
-            {
-                currentUser = _userService.GetUserById(Constants.Security.SuperUserId);
-            }
+        return analyticsLevel;
+    }
 
-            _logger.LogInformation("Telemetry level set to {telemetryLevel} by {username}", telemetryLevel, currentUser?.Username);
-            _keyValueService.SetValue(Key, telemetryLevel.ToString());
+    public void SetConsentLevel(TelemetryLevel telemetryLevel)
+    {
+        IUser? 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());
     }
 }
diff --git a/src/Umbraco.Core/Services/MoveOperationStatusType.cs b/src/Umbraco.Core/Services/MoveOperationStatusType.cs
index 4de17b2fa5e6..26e70eb9e0f9 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
+{
     /// 
-    /// A status type of the result of moving an item
+    ///     The move was successful.
     /// 
-    /// 
-    /// Anything less than 10 = Success!
-    /// 
-    public enum MoveOperationStatusType : byte
-    {
-        /// 
-        /// The move was successful.
-        /// 
-        Success = 0,
+    Success = 0,
 
-        /// 
-        /// The parent being moved to doesn't exist
-        /// 
-        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 7298d7f23a4b..cf7417058e0e 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;
+    private readonly INodeCountRepository _nodeCountRepository;
+    private readonly ICoreScopeProvider _scopeProvider;
 
-        public NodeCountService(INodeCountRepository nodeCountRepository, ICoreScopeProvider scopeProvider)
-        {
-            _nodeCountRepository = nodeCountRepository;
-            _scopeProvider = scopeProvider;
-        }
+    public NodeCountService(INodeCountRepository nodeCountRepository, ICoreScopeProvider scopeProvider)
+    {
+        _nodeCountRepository = nodeCountRepository;
+        _scopeProvider = scopeProvider;
+    }
 
-        public int GetNodeCount(Guid nodeType)
-        {
-            using var scope = _scopeProvider.CreateCoreScope(autoComplete: true);
-            return _nodeCountRepository.GetNodeCount(nodeType);
-        }
+    public int GetNodeCount(Guid nodeType)
+    {
+        using ICoreScope 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 39aa6a863da8..822ba890794b 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.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;
-
-        public NotificationService(ICoreScopeProvider provider, IUserService userService, IContentService contentService, ILocalizationService localizationService,
-            ILogger logger, IIOHelper ioHelper, INotificationsRepository notificationsRepository, IOptions globalSettings, IOptions contentSettings, IEmailSender emailSender)
-        {
-            _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;
-        }
+        _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;
+    }
 
-        /// 
-        /// 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]);
-        }
+    /// 
+    ///     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();
 
-        /// 
-        /// 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)
+        // exit if there are no entities
+        if (entitiesL.Count == 0)
         {
-            var entitiesL = entities.ToList();
+            return;
+        }
 
-            //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();
 
-            //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();
 
-            // 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
+        {
+            // 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)
+            {
+                break;
+            }
 
-            // see notes above
-            var id = Cms.Core.Constants.Security.SuperUserId;
-            const int pagesz = 400; // load batches of 400 users
-            do
+            var i = 0;
+            foreach (IUser user in users)
             {
-                // 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;
+                // continue if there's no notification for this user
+                if (notifications[i].UserId != user.Id)
+                {
+                    continue; // next user
+                }
 
-                var i = 0;
-                foreach (var user in users)
+                for (var j = 0; j < entitiesL.Count; j++)
                 {
-                    // continue if there's no notification for this user
-                    if (notifications[i].UserId != user.Id) continue; // next user
+                    IContent content = entitiesL[j];
+                    var path = paths[j];
 
-                    for (var j = 0; j < entitiesL.Count; 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);
+                        prevVersionDictionary[content.Id] = GetPreviousVersion(content.Id);
+                    }
 
-                    if (i >= notifications.Count) break; // break if no more notifications
+                    // queue notification
+                    NotificationRequest req = CreateNotificationRequest(operatingUser, user, content, prevVersionDictionary[content.Id], actionName, siteUri, createSubject, createBody);
+                    Enqueue(req);
                 }
 
-                // load more users if any
-                id = users.Count == pagesz ? users.Last().Id + 1 : -1;
+                // skip other notifications for this user, essentially this means moving i to the next index of notifications
+                // for the next user.
+                do
+                {
+                    i++;
+                }
+                while (i < notifications.Count && notifications[i].UserId == user.Id);
 
-            } while (id > 0);
+                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)
+    /// 
+    ///     Gets the notifications for the user
+    /// 
+    /// 
+    /// 
+    public IEnumerable? GetUserNotifications(IUser user)
+    {
+        using (ICoreScope scope = _uowProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = _uowProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _notificationsRepository.GetUsersNotifications(userIds, action, nodeIds, objectType);
-            }
+            return _notificationsRepository.GetUserNotifications(user);
         }
-        /// 
-        /// Gets the notifications for the user
-        /// 
-        /// 
-        /// 
-        public 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)
+    /// 
+    public IEnumerable? GetUserNotifications(IUser? user, string path)
+    {
+        if (user is null)
         {
-            using (var scope = _uowProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _notificationsRepository.GetUserNotifications(user);
-            }
+            return null;
         }
 
-        /// 
-        /// 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)
+        IEnumerable? userNotifications = GetUserNotifications(user);
+        return FilterUserNotificationsByPath(userNotifications, path);
+    }
+
+    /// 
+    ///     Deletes notifications by entity
+    /// 
+    /// 
+    public IEnumerable? GetEntityNotifications(IEntity entity)
+    {
+        using (ICoreScope scope = _uowProvider.CreateCoreScope(autoComplete: true))
         {
-            if (user is null)
-            {
-                return null;
-            }
+            return _notificationsRepository.GetEntityNotifications(entity);
+        }
+    }
 
-            var userNotifications = GetUserNotifications(user);
-            return FilterUserNotificationsByPath(userNotifications, path);
+    /// 
+    ///     Deletes notifications by entity
+    /// 
+    /// 
+    public void DeleteNotifications(IEntity entity)
+    {
+        using (ICoreScope scope = _uowProvider.CreateCoreScope())
+        {
+            _notificationsRepository.DeleteNotifications(entity);
+            scope.Complete();
         }
+    }
 
-        /// 
-        /// Filters a userNotifications collection by a path
-        /// 
-        /// 
-        /// 
-        /// 
-        public IEnumerable? FilterUserNotificationsByPath(IEnumerable? userNotifications, string path)
+    /// 
+    ///     Deletes notifications by user
+    /// 
+    /// 
+    public void DeleteNotifications(IUser user)
+    {
+        using (ICoreScope scope = _uowProvider.CreateCoreScope())
         {
-            var pathParts = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
-            return userNotifications?.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList();
+            _notificationsRepository.DeleteNotifications(user);
+            scope.Complete();
         }
+    }
 
-        /// 
-        /// Deletes notifications by entity
-        /// 
-        /// 
-        public IEnumerable? GetEntityNotifications(IEntity entity)
+    /// 
+    ///     Delete notifications by user and entity
+    /// 
+    /// 
+    /// 
+    public void DeleteNotifications(IUser user, IEntity entity)
+    {
+        using (ICoreScope scope = _uowProvider.CreateCoreScope())
         {
-            using (var scope = _uowProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _notificationsRepository.GetEntityNotifications(entity);
-            }
+            _notificationsRepository.DeleteNotifications(user, entity);
+            scope.Complete();
         }
+    }
 
-        /// 
-        /// Deletes notifications by entity
-        /// 
-        /// 
-        public void DeleteNotifications(IEntity entity)
+    /// 
+    ///     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)
         {
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                _notificationsRepository.DeleteNotifications(entity);
-                scope.Complete();
-            }
+            return null;
         }
 
-        /// 
-        /// Deletes notifications by user
-        /// 
-        /// 
-        public void DeleteNotifications(IUser user)
+        using (ICoreScope scope = _uowProvider.CreateCoreScope())
         {
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                _notificationsRepository.DeleteNotifications(user);
-                scope.Complete();
-            }
+            IEnumerable notifications = _notificationsRepository.SetNotifications(user, entity, actions);
+            scope.Complete();
+            return notifications;
         }
+    }
 
-        /// 
-        /// Delete notifications by user and entity
-        /// 
-        /// 
-        /// 
-        public void DeleteNotifications(IUser user, IEntity entity)
+    /// 
+    ///     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())
         {
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                _notificationsRepository.DeleteNotifications(user, entity);
-                scope.Complete();
-            }
+            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]);
+    }
 
-        /// 
-        /// Sets the specific notifications for the user and entity
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This performs a full replace
-        /// 
-        public IEnumerable? SetNotifications(IUser? user, IEntity entity, string[] actions)
+    private IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, IEnumerable nodeIds, Guid objectType)
+    {
+        using (ICoreScope scope = _uowProvider.CreateCoreScope(autoComplete: true))
         {
-            if (user is null)
-            {
-                return null;
-            }
+            return _notificationsRepository.GetUsersNotifications(userIds, action, nodeIds, objectType);
+        }
+    }
 
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                var notifications = _notificationsRepository.SetNotifications(user, entity, actions);
-                scope.Complete();
-                return notifications;
-            }
+    /// 
+    ///     Replaces the HTML symbols with the character equivalent.
+    /// 
+    /// The old string.
+    private static void ReplaceHtmlSymbols(ref string? oldString)
+    {
+        if (oldString.IsNullOrWhiteSpace())
+        {
+            return;
         }
 
-        /// 
-        /// Creates a new notification
-        /// 
-        /// 
-        /// 
-        /// The action letter - note: this is a string for future compatibility
-        /// 
-        public Notification CreateNotification(IUser user, IEntity entity, string action)
+        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)
         {
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                var notification = _notificationsRepository.CreateNotification(user, entity, action);
-                scope.Complete();
-                return notification;
-            }
+            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");
         }
 
-        #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 (createSubject == null)
         {
-            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");
+            throw new ArgumentNullException("createSubject");
+        }
+
+        if (createBody == null)
+        {
+            throw new ArgumentNullException("createBody");
+        }
 
-            // build summary
-            var summary = new StringBuilder();
+        // build summary
+        var summary = new StringBuilder();
 
-            if (content.ContentType.VariesByNothing())
+        if (content.ContentType.VariesByNothing())
+        {
+            if (!_contentSettings.Notifications.DisableHtmlEmail)
             {
-                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)
                 {
-                    //create the HTML summary for invariant content
+                    // TODO: doesn't take into account variants
+                    var newText = p.GetValue() != null ? p.GetValue()?.ToString() : string.Empty;
+                    var oldText = newText;
 
-                    //list all of the property values like we used to
-                    summary.Append("
"); - foreach (var p in content.Properties) + // check if something was changed and display the changes otherwise display the fields + if (oldDoc?.Properties.Contains(p.PropertyType.Alias) ?? false) { - // 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(""); + 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); } - summary.Append("
"); - summary.Append(p.PropertyType.Name); - summary.Append(""); - summary.Append(newText); - summary.Append("
"); + + // show the values + summary.Append(""); + summary.Append( + ""); + summary.Append(p.PropertyType.Name); + summary.Append(""); + summary.Append(""); + summary.Append(newText); + summary.Append(""); + summary.Append(""); } + summary.Append(""); } - else if (content.ContentType.VariesByCulture()) + } + else if (content.ContentType.VariesByCulture()) + { + // it's variant, so detect what cultures have changed + if (!_contentSettings.Notifications.DisableHtmlEmail) { - //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) { - //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) { - foreach (var culture in culturesChanged) - { - summary.Append("
    • "); - summary.Append(culture); - summary.Append("
    • "); - } + summary.Append("
    • "); + summary.Append(culture); + summary.Append("
    • "); } - - summary.Append("
    "); } - else - { - //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("'"); - } + 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 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 fromMail = _contentSettings.Notifications.Email ?? _globalSettings.Smtp?.From; - - var subject = createSubject((mailingUser, subjectVars)); - var body = ""; - var isBodyHtml = false; - - if (_contentSettings.Notifications.DisableHtmlEmail) - { - body = createBody((user: mailingUser, body: bodyVars, false)); - } - else - { - isBodyHtml = true; - body = - string.Concat(@" + var protocol = _globalSettings.UseHttps ? "https" : "http"; + + 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 fromMail = _contentSettings.Notifications.Email ?? _globalSettings.Smtp?.From; + + 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))); - } - - // 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); +", + createBody((user: mailingUser, body: bodyVars, true))); } - private string ReplaceLinks(string text, Uri siteUri) + // 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 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; + var serverName = siteUri.Host; + body = body.Replace( + $"http://{serverName}", + $"https://{serverName}"); } - /// - /// 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(""", "\""); - } + // create the mail message + var mail = new EmailMessage(fromMail, mailingUser.Email, subject, body, isBodyHtml); - // manage notifications - // ideally, would need to use IBackgroundTasks - but they are not part of Core! + return new NotificationRequest(mail, actionName, mailingUser.Name, mailingUser.Email); + } - private static readonly object Locker = new object(); - private static readonly BlockingCollection Queue = new BlockingCollection(); - private static volatile bool _running; + 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 void Enqueue(NotificationRequest notification) + private static readonly BlockingCollection Queue = new(); + private static volatile bool _running; + + private void Enqueue(NotificationRequest notification) + { + Queue.Add(notification); + if (_running) { - Queue.Add(notification); - if (_running) return; - lock (Locker) - { - if (_running) return; - Process(Queue); - _running = true; - } + return; } - private class NotificationRequest + lock (Locker) { - public NotificationRequest(EmailMessage mail, string? action, string? userName, string? email) + if (_running) { - Mail = mail; - Action = action; - UserName = userName; - Email = email; + return; } - public EmailMessage Mail { get; } - - public string? Action { get; } - - public string? UserName { get; } - - public string? Email { get; } + Process(Queue); + _running = true; } + } - private void Process(BlockingCollection notificationRequests) + private void Process(BlockingCollection notificationRequests) => + ThreadPool.QueueUserWorkItem(state => { - ThreadPool.QueueUserWorkItem(state => + _logger.LogDebug("Begin processing notifications."); + while (true) { - _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) + { + _logger.LogError(ex, "An error occurred sending notification"); + } + } + + lock (Locker) + { + if (notificationRequests.Count > 0) { - if (notificationRequests.Count > 0) continue; // last chance - _running = false; // going down - break; + continue; // last chance } + + _running = false; // going down + break; } + } + + _logger.LogDebug("Done processing notifications."); + }); - _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 a69dc6ee126b..919077919c4b 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 - - /// - /// 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 + static OperationResult() { - /// - /// Initializes a new instance of the class. - /// - public OperationResult(TResultType result, EventMessages? eventMessages) + // 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) { - Result = result; - EventMessages = eventMessages; + throw new InvalidOperationException($"Type {type} is not an enum."); } - static OperationResult() + if (Enum.GetUnderlyingType(type) != typeof(byte)) { - // 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 ."); + 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 + /// + /// Initializes a new instance of the class. + /// + public OperationResult(TResultType result, EventMessages? eventMessages) + { + Result = result; + EventMessages = eventMessages; + } - /// - /// Gets the result of the operation. - /// - public TResultType Result { get; } + /// + /// 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 event messages produced by the operation. - /// - public EventMessages? EventMessages { get; } + /// + /// Gets the result of the operation. + /// + public TResultType Result { 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 + public OperationResult(TResultType result, EventMessages? eventMessages, TEntity? entity) + : base(result, eventMessages) => + Entity = entity; + + /// + /// Gets the entity. + /// + public TEntity? Entity { get; } +} + +/// +/// +/// Represents the default operation result. +/// +public class OperationResult : OperationResult +{ + /// + /// + /// Initializes a new instance of the class with a status and event messages. + /// + /// The status of the operation. + /// Event messages produced by the operation. + public OperationResult(OperationResultType result, EventMessages eventMessages) + : base(result, eventMessages) { - /// - /// - /// 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 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. + /// Creates a successful operation attempt. /// - public OperationResult(TResultType result, EventMessages? eventMessages, TEntity? entity) - : base(result, eventMessages) - { - Entity = entity; - } + /// 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 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)); /// - /// Gets the entity. + /// Creates a successful operation attempt indicating that nothing was done. /// - public TEntity? Entity { get; } - } + /// 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)); - /// - /// - /// Represents the default operation result. - /// - public class OperationResult : OperationResult - { - /// /// - /// Initializes a new instance of the class with a status and event messages. + /// Creates a failed operation attempt indicating that the operation has been cancelled. /// - /// 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 Cancel(EventMessages eventMessages) => + Core.Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages)); - public static OperationResult Succeed(EventMessages eventMessages) - { - return new OperationResult(OperationResultType.Success, eventMessages); - } + public static Attempt?> + Cancel(EventMessages eventMessages) => Core.Attempt.Fail( + new OperationResult( + OperationResultType.FailedCancelledByEvent, + eventMessages)); - public static OperationResult Cancel(EventMessages eventMessages) - { - return new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages); - } + public static Attempt?> + Cancel(EventMessages eventMessages, TValue value) => Core.Attempt.Fail( + new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages, value)); - // 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 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) { - /// - /// 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?> Succeed(EventMessages eventMessages) - { - return Core.Attempt.Succeed(new OperationResult(OperationResultType.Success, eventMessages)); - } - - public static Attempt?> Succeed(EventMessages eventMessages, TValue value) - { - return Core.Attempt.Succeed(new OperationResult(OperationResultType.Success, eventMessages, value)); - } - - public static Attempt?> Succeed(TStatusType statusType, EventMessages eventMessages) - where TStatusType : struct - { - return Core.Attempt.Succeed(new OperationResult(statusType, eventMessages)); - } - - public static Attempt?> Succeed(TStatusType statusType, EventMessages eventMessages, TValue value) - where TStatusType : struct - { - return 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) - { - return 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) - { - 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)); - } + eventMessages.Add(new EventMessage(string.Empty, exception.Message, EventMessageType.Error)); + return Core.Attempt.Fail( + new OperationResult(OperationResultType.FailedExceptionThrown, eventMessages), + exception); } + + public static Attempt?> + Fail(EventMessages eventMessages, Exception exception) => Core.Attempt.Fail( + new OperationResult(OperationResultType.FailedExceptionThrown, eventMessages), + exception); + + public static Attempt?> Fail( + TStatusType statusType, + EventMessages eventMessages) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, 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?> Fail( + TStatusType statusType, + EventMessages eventMessages) + where TStatusType : struct => + Core.Attempt.Fail(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?> Fail( + TStatusType statusType, + EventMessages eventMessages, + Exception exception) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, eventMessages), exception); + + public static Attempt?> Fail( + TStatusType statusType, + EventMessages eventMessages, + TValue value, + Exception exception) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, eventMessages, value), exception); + + 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 15b332e43c35..c87b70c2a208 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. + + /// + /// The operation was successful. + /// + Success = 0, + + /// + /// 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 has been cancelled by an event handler. + /// + FailedCancelledByEvent = Failed | 4, + /// - /// A value indicating the result of an operation. + /// The operation could not complete due to an exception. /// - 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. - - /// - /// The operation was successful. - /// - Success = 0, - - /// - /// 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 has been cancelled by an event handler. - /// - FailedCancelledByEvent = Failed | 4, - - /// - /// 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? - - // TODO: In the future, we might need to add more operations statuses, potentially like 'FailedByPermissions', etc... - } + 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? + + // 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 513654428bbe..39c89e5c4a43 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; - } + 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 name of the ordering field. + /// + public string? OrderBy { get; } - /// - /// Gets the default instance. - /// - public static Ordering ByDefault() - => DefaultOrdering; + /// + /// Gets the ordering direction. + /// + public Direction Direction { get; } - /// - /// Gets the name of the ordering field. - /// - public string? OrderBy { get; } + /// + /// Gets (ISO) culture to consider when sorting multi-lingual fields. + /// + public string? Culture { get; } - /// - /// Gets the ordering direction. - /// - public Direction Direction { get; } + /// + /// Gets a value indicating whether the ordering field is a custom user property. + /// + public bool IsCustomField { get; } - /// - /// Gets (ISO) culture to consider when sorting multi-lingual fields. - /// - public string? Culture { 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 ordering field is a custom user property. - /// - public bool IsCustomField { get; } + /// + /// Gets a value indicating whether the culture of this ordering is invariant. + /// + public bool IsInvariant => this == DefaultOrdering || Culture == string.Empty; - /// - /// Gets a value indicating whether this ordering is the default ordering. - /// - public bool IsEmpty => this == DefaultOrdering || OrderBy == null; + /// + /// 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 a value indicating whether the culture of this ordering is invariant. - /// - public bool IsInvariant => this == DefaultOrdering || Culture == string.Empty; - } + /// + /// 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 9a368dab7e35..39751dad61c0 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 NumberOfInstructionsProcessed { get; private set; } - public int LastId { get; private set; } + public int LastId { get; private set; } - public bool InstructionsWerePruned { 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 AsCompleted(int numberOfInstructionsProcessed, int lastId) => + new() { NumberOfInstructionsProcessed = numberOfInstructionsProcessed, LastId = lastId }; - public static ProcessInstructionsResult AsCompletedAndPruned(int numberOfInstructionsProcessed, int lastId) => - new ProcessInstructionsResult { NumberOfInstructionsProcessed = numberOfInstructionsProcessed, LastId = lastId, InstructionsWerePruned = true }; - }; + public static ProcessInstructionsResult AsCompletedAndPruned(int numberOfInstructionsProcessed, int lastId) => + new() + { + NumberOfInstructionsProcessed = numberOfInstructionsProcessed, + LastId = lastId, + InstructionsWerePruned = true, + }; } diff --git a/src/Umbraco.Core/Services/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs index c5a431277633..d93cbd4a7ccf 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) + { + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _textService = textService; + _valueEditorCache = valueEditorCache; + } + + /// + public IEnumerable ValidatePropertyValue( + IPropertyType propertyType, + object? postedValue) { - private readonly PropertyEditorCollection _propertyEditors; - private readonly IDataTypeService _dataTypeService; - private readonly ILocalizedTextService _textService; - private readonly IValueEditorCache _valueEditorCache; - - public PropertyValidationService( - PropertyEditorCollection propertyEditors, - IDataTypeService dataTypeService, - ILocalizedTextService textService, - IValueEditorCache valueEditorCache) + 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); + throw new InvalidOperationException("No data type found by id " + propertyType.DataTypeId); + } - return ValidatePropertyValue(editor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage); + IDataEditor? editor = _propertyEditors[propertyType.PropertyEditorAlias]; + if (editor == null) + { + throw new InvalidOperationException("No property editor found by alias " + + propertyType.PropertyEditorAlias); } - /// - public IEnumerable ValidatePropertyValue( - IDataEditor editor, - IDataType dataType, - object? postedValue, - bool isRequired, - string? validationRegExp, - string? isRequiredMessage, - string? validationRegExpMessage) + 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)) { - // 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 (var 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; + validationResult.ErrorMessage = isRequiredMessage; } - } - /// - public bool IsPropertyDataValid(IContent content, out IProperty[] invalidProperties, CultureImpact? impact) - { - // select invalid properties - invalidProperties = content.Properties.Where(x => + if (!string.IsNullOrWhiteSpace(validationRegExp) && !string.IsNullOrWhiteSpace(validationRegExpMessage) && + formatDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase)) { - var propertyTypeVaries = x.PropertyType.VariesByCulture(); + validationResult.ErrorMessage = validationRegExpMessage; + } - if (impact is null) - { - return false; - } - // impacts invariant = validate invariant property, invariant culture - if (impact.ImpactsOnlyInvariantCulture) - return !(propertyTypeVaries || IsPropertyValid(x, null)); + yield return validationResult; + } + } - // impacts all = validate property, all cultures (incl. invariant) - if (impact.ImpactsAllCultures) - return !IsPropertyValid(x); + /// + public bool IsPropertyDataValid(IContent content, out IProperty[] invalidProperties, CultureImpact? impact) + { + // select invalid properties + invalidProperties = content.Properties.Where(x => + { + var propertyTypeVaries = x.PropertyType.VariesByCulture(); - // impacts explicit culture = validate variant property, explicit culture - if (propertyTypeVaries) - return !IsPropertyValid(x, impact.Culture); + if (impact is null) + { + return false; + } - // 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 + // impacts invariant = validate invariant property, invariant culture + if (impact.ImpactsOnlyInvariantCulture) + { + return !(propertyTypeVaries || IsPropertyValid(x, null)); + } - var alsoInvariant = impact.ImpactsAlsoInvariantProperties || !content.Published; - return alsoInvariant && !IsPropertyValid(x, null); + // impacts all = validate property, all cultures (incl. invariant) + if (impact.ImpactsAllCultures) + { + return !IsPropertyValid(x); + } - }).ToArray(); + // impacts explicit culture = validate variant property, explicit culture + if (propertyTypeVaries) + { + return !IsPropertyValid(x, impact.Culture); + } - return invalidProperties.Length == 0; - } + // 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(); - /// - 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. + return invalidProperties.Length == 0; + } - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); + /// + 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; + 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)) + // 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)) { - // 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; + return false; } + } - // if validating only invariant/neutral, we are good - if (culture == null && segment == null) - return true; + // 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; + // 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) + // 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) + // 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); + // 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(); + // 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)); - } + 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) + /// + /// 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) { - return IsPropertyValueValid(property.PropertyType, value); + // 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; } - /// - /// 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) - { - // 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; - var valueEditor = editor.GetValueEditor(configuration); - return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp).Any(); - } + 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 b6216e4b5830..6f3de02c55d8 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.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; + return _publicAccessRepository.GetMany(); } + } - /// - /// 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) => + 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(); - /// - /// 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)); - } + // start with the deepest id + ids.Reverse(); - /// - /// 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) + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - //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) { - //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) { - var found = entries.FirstOrDefault(x => x.ProtectedNodeId == id); - if (found != null) return found; + 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); - } + return null; + } - /// - /// 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); - } + /// + /// 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); + } - /// - /// Adds a rule - /// - /// - /// - /// - /// - public Attempt?> AddRule(IContent content, string ruleType, string ruleValue) + /// + /// 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()) { - var evtMsgs = EventMessagesFactory.Get(); - PublicAccessEntry? entry; - using (var scope = ScopeProvider.CreateCoreScope()) + entry = _publicAccessRepository.GetMany().FirstOrDefault(x => x.ProtectedNodeId == content.Id); + if (entry == null) { - 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); - } + return OperationResult.Attempt.Cannot(evtMsgs); // causes rollback + } - _publicAccessRepository.Save(entry); + 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(); - - scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + return OperationResult.Attempt.Cancel(evtMsgs, entry); } - return OperationResult.Attempt.Succeed(evtMsgs, entry); + _publicAccessRepository.Save(entry); + + scope.Complete(); + + scope.Notifications.Publish( + new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); } - /// - /// Removes a rule - /// - /// - /// - /// - public Attempt RemoveRule(IContent content, string ruleType, string ruleValue) + 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()) { - var evtMsgs = EventMessagesFactory.Get(); - PublicAccessEntry? entry; - using (var scope = ScopeProvider.CreateCoreScope()) + entry = _publicAccessRepository.GetMany().FirstOrDefault(x => x.ProtectedNodeId == content.Id); + if (entry == null) { - 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 + return Attempt.Fail(); // causes rollback // causes rollback + } - entry.RemoveRule(existingRule); + PublicAccessRule? existingRule = + entry.Rules.FirstOrDefault(x => x.RuleType == ruleType && x.RuleValue == ruleValue); + if (existingRule == null) + { + return Attempt.Fail(); // causes rollback // causes rollback + } - var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotifiation)) - { - scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs); - } + entry.RemoveRule(existingRule); - _publicAccessRepository.Save(entry); + var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotifiation)) + { scope.Complete(); - - scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + return OperationResult.Attempt.Cancel(evtMsgs); } - return OperationResult.Attempt.Succeed(evtMsgs); + _publicAccessRepository.Save(entry); + scope.Complete(); + + scope.Notifications.Publish( + new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); } - /// - /// Saves the entry - /// - /// - public Attempt Save(PublicAccessEntry entry) - { - var evtMsgs = EventMessagesFactory.Get(); + return OperationResult.Attempt.Succeed(evtMsgs); + } - using (var scope = ScopeProvider.CreateCoreScope()) - { - var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotifiation)) - { - scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs); - } + /// + /// Saves the entry + /// + /// + public Attempt Save(PublicAccessEntry entry) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); - _publicAccessRepository.Save(entry); + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotifiation)) + { scope.Complete(); - - scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + return OperationResult.Attempt.Cancel(evtMsgs); } - return OperationResult.Attempt.Succeed(evtMsgs); + _publicAccessRepository.Save(entry); + scope.Complete(); + + scope.Notifications.Publish( + new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); } - /// - /// Deletes the entry and all associated rules - /// - /// - public Attempt Delete(PublicAccessEntry entry) - { - var evtMsgs = EventMessagesFactory.Get(); + return OperationResult.Attempt.Succeed(evtMsgs); + } - using (var scope = ScopeProvider.CreateCoreScope()) - { - var deletingNotification = new PublicAccessEntryDeletingNotification(entry, evtMsgs); - if (scope.Notifications.PublishCancelable(deletingNotification)) - { - scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs); - } + /// + /// Deletes the entry and all associated rules + /// + /// + public Attempt Delete(PublicAccessEntry entry) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); - _publicAccessRepository.Delete(entry); + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var deletingNotification = new PublicAccessEntryDeletingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(deletingNotification)) + { scope.Complete(); - - scope.Notifications.Publish(new PublicAccessEntryDeletedNotification(entry, evtMsgs).WithStateFrom(deletingNotification)); + return OperationResult.Attempt.Cancel(evtMsgs); } - return OperationResult.Attempt.Succeed(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 d8a7f201de2c..eb42dcda73fa 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; + return false; + } - var allEntries = publicAccessService.GetAll(); + IEnumerable allEntries = publicAccessService.GetAll(); - foreach (var entry in allEntries) + foreach (PublicAccessEntry entry in allEntries) + { + // 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) { - //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); - } + // a rule is being updated so flag this entry to be saved + roleRule.RuleValue = newRolename ?? string.Empty; + save = true; } - return hasChange; + if (save) + { + hasChange = true; + publicAccessService.Save(entry); + } } - public static bool HasAccess(this IPublicAccessService publicAccessService, int documentId, IContentService contentService, string username, IEnumerable currentMemberRoles) - { - var content = contentService.GetById(documentId); - if (content == null) return true; - - var entry = publicAccessService.GetEntryForContent(content); - if (entry == null) return true; + return hasChange; + } - return HasAccess(entry, username, currentMemberRoles); + 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; } - /// - /// 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) + PublicAccessEntry? entry = publicAccessService.GetEntryForContent(content); + if (entry == null) { - 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"); + return true; + } + + return HasAccess(entry, username, currentMemberRoles); + } - var entry = publicAccessService.GetEntryForContent(path.EnsureEndsWith(path)); - if (entry == null) return true; + /// + /// 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"); + } - var roles = await rolesCallback(); + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentException("Value cannot be null or whitespace.", "username"); + } - return HasAccess(entry, username, roles); + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Value cannot be null or whitespace.", "path"); } - private static bool HasAccess(PublicAccessEntry entry, string username, IEnumerable roles) + PublicAccessEntry? entry = publicAccessService.GetEntryForContent(path.EnsureEndsWith(path)); + if (entry == null) { - if (entry is null) - { - throw new ArgumentNullException(nameof(entry)); - } + return true; + } - if (string.IsNullOrEmpty(username)) - { - throw new ArgumentException($"'{nameof(username)}' cannot be null or empty.", nameof(username)); - } + IEnumerable roles = await rolesCallback(); - if (roles is null) - { - throw new ArgumentNullException(nameof(roles)); - } + return HasAccess(entry, username, roles); + } + + private static bool HasAccess(PublicAccessEntry entry, string username, IEnumerable roles) + { + if (entry is null) + { + throw new ArgumentNullException(nameof(entry)); + } - 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)) - ); + 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 0ab820e7a65c..f689249afca7 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 +{ /// - /// Represents the result of publishing a document. + /// Initializes a new instance of the class. /// - public class PublishResult : OperationResult + public PublishResult(PublishResultType resultType, EventMessages? eventMessages, IContent? content) + : base(resultType, 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) - { } + /// + /// 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 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 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 43fab58218b9..b8ebd5edd49c 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 + + /// + /// The document was successfully published. + /// + SuccessPublish = 0, + + /// + /// The specified document culture was successfully published. + /// + SuccessPublishCulture = 1, + + /// + /// The document was already published. + /// + SuccessPublishAlready = 2, + + #endregion + + #region Success - Unpublish + + /// + /// The document was successfully unpublished. + /// + SuccessUnpublish = 3, + /// - /// A value indicating the result of publishing or unpublishing a document. + /// The document was already unpublished. /// - 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 - - /// - /// The document was successfully published. - /// - SuccessPublish = 0, - - /// - /// The specified document culture was successfully published. - /// - SuccessPublishCulture = 1, - - /// - /// The document was already published. - /// - SuccessPublishAlready = 2, - - #endregion - - #region Success - Unpublish - - /// - /// The document was successfully unpublished. - /// - SuccessUnpublish = 3, - - /// - /// The document was already unpublished. - /// - SuccessUnpublishAlready = 4, - - /// - /// The specified document culture was unpublished, the document item itself remains published. - /// - SuccessUnpublishCulture = 5, - - /// - /// 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, and was the last published culture in the document, therefore the document itself was unpublished. - /// - SuccessUnpublishLastCulture = 8, - - #endregion - - #region Success - Mixed - - /// - /// Specified document cultures were successfully published and unpublished (in the same operation). - /// - SuccessMixedCulture = 7, - - #endregion - - #region Failed - Publish - - /// - /// The operation failed. - /// - /// All values above this value indicate a failure. - FailedPublish = 128, - - /// - /// The document could not be published because its ancestor path is not published. - /// - FailedPublishPathNotPublished = FailedPublish | 1, - - /// - /// 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 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 has expired so we cannot force it to be - /// published again as part of a bulk publish operation. - /// - FailedPublishCultureHasExpired = FailedPublish | 4, - - /// - /// 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 document could not be published because it is in the trash. - /// - FailedPublishIsTrashed = FailedPublish | 6, - - /// - /// The publish action has been cancelled by an event handler. - /// - FailedPublishCancelledByEvent = FailedPublish | 7, - - /// - /// 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 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 some mandatory cultures are missing. - /// - FailedPublishMandatoryCultureMissing = FailedPublish | 10, // in ContentService.SavePublishing - - /// - /// The document could not be published because it has been modified by another user. - /// - FailedPublishConcurrencyViolation = FailedPublish | 11, - - #endregion - - #region Failed - Unpublish - - /// - /// 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 - } + SuccessUnpublishAlready = 4, + + /// + /// The specified document culture was unpublished, the document item itself remains published. + /// + SuccessUnpublishCulture = 5, + + /// + /// 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, and was the last published culture in the document, therefore the + /// document itself was unpublished. + /// + SuccessUnpublishLastCulture = 8, + + #endregion + + #region Success - Mixed + + /// + /// Specified document cultures were successfully published and unpublished (in the same operation). + /// + SuccessMixedCulture = 7, + + #endregion + + #region Failed - Publish + + /// + /// The operation failed. + /// + /// All values above this value indicate a failure. + FailedPublish = 128, + + /// + /// The document could not be published because its ancestor path is not published. + /// + FailedPublishPathNotPublished = FailedPublish | 1, + + /// + /// 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 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 has expired so we cannot force it to be + /// published again as part of a bulk publish operation. + /// + FailedPublishCultureHasExpired = FailedPublish | 4, + + /// + /// 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 document could not be published because it is in the trash. + /// + FailedPublishIsTrashed = FailedPublish | 6, + + /// + /// The publish action has been cancelled by an event handler. + /// + FailedPublishCancelledByEvent = FailedPublish | 7, + + /// + /// 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 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 some mandatory cultures are missing. + /// + FailedPublishMandatoryCultureMissing = FailedPublish | 10, // in ContentService.SavePublishing + + /// + /// The document could not be published because it has been modified by another user. + /// + FailedPublishConcurrencyViolation = FailedPublish | 11, + + #endregion + + #region Failed - Unpublish + + /// + /// 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 } diff --git a/src/Umbraco.Core/Services/RedirectUrlService.cs b/src/Umbraco.Core/Services/RedirectUrlService.cs index 14c3e834bfbb..e68eed31e779 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; + private readonly IRedirectUrlRepository _redirectUrlRepository; - public RedirectUrlService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, - IRedirectUrlRepository redirectUrlRepository) - : base(provider, loggerFactory, eventMessagesFactory) - { - _redirectUrlRepository = 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) + public void Register(string url, Guid contentKey, string? culture = null) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (var scope = ScopeProvider.CreateCoreScope()) + IRedirectUrl? redir = _redirectUrlRepository.Get(url, contentKey, culture); + if (redir != null) + { + redir.CreateDateUtc = DateTime.UtcNow; + } + else { - 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 = new RedirectUrl { Key = Guid.NewGuid(), Url = url, ContentKey = contentKey, Culture = culture }; } + + _redirectUrlRepository.Save(redir); + scope.Complete(); } + } - public void Delete(IRedirectUrl redirectUrl) + public void Delete(IRedirectUrl redirectUrl) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (var scope = ScopeProvider.CreateCoreScope()) - { - _redirectUrlRepository.Delete(redirectUrl); - scope.Complete(); - } + _redirectUrlRepository.Delete(redirectUrl); + scope.Complete(); } + } - public void Delete(Guid id) + public void Delete(Guid id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (var scope = ScopeProvider.CreateCoreScope()) - { - _redirectUrlRepository.Delete(id); - scope.Complete(); - } + _redirectUrlRepository.Delete(id); + scope.Complete(); } + } - public void DeleteContentRedirectUrls(Guid contentKey) + public void DeleteContentRedirectUrls(Guid contentKey) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (var scope = ScopeProvider.CreateCoreScope()) - { - _redirectUrlRepository.DeleteContentUrls(contentKey); - scope.Complete(); - } + _redirectUrlRepository.DeleteContentUrls(contentKey); + scope.Complete(); } + } - public void DeleteAll() + public void DeleteAll() + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (var scope = ScopeProvider.CreateCoreScope()) - { - _redirectUrlRepository.DeleteAll(); - scope.Complete(); - } + _redirectUrlRepository.DeleteAll(); + scope.Complete(); } + } - public IRedirectUrl? GetMostRecentRedirectUrl(string url) + public IRedirectUrl? GetMostRecentRedirectUrl(string url) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetMostRecentUrl(url); - } + return _redirectUrlRepository.GetMostRecentUrl(url); } + } - public IEnumerable GetContentRedirectUrls(Guid contentKey) + public IEnumerable GetContentRedirectUrls(Guid contentKey) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetContentUrls(contentKey); - } + return _redirectUrlRepository.GetContentUrls(contentKey); } + } - public IEnumerable GetAllRedirectUrls(long pageIndex, int pageSize, out long total) + public IEnumerable GetAllRedirectUrls(long pageIndex, int pageSize, out long total) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetAllUrls(pageIndex, pageSize, out total); - } + return _redirectUrlRepository.GetAllUrls(pageIndex, pageSize, out total); } + } - public IEnumerable GetAllRedirectUrls(int rootContentId, long pageIndex, int pageSize, out long total) + public IEnumerable GetAllRedirectUrls(int rootContentId, long pageIndex, int pageSize, out long total) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetAllUrls(rootContentId, pageIndex, pageSize, out total); - } + return _redirectUrlRepository.GetAllUrls(rootContentId, pageIndex, pageSize, out total); } + } - public IEnumerable SearchRedirectUrls(string searchTerm, long pageIndex, int pageSize, out long total) + public IEnumerable SearchRedirectUrls(string searchTerm, long pageIndex, int pageSize, out long total) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.SearchUrls(searchTerm, pageIndex, pageSize, out total); - } + return _redirectUrlRepository.SearchUrls(searchTerm, pageIndex, pageSize, out total); } + } - public IRedirectUrl? GetMostRecentRedirectUrl(string url, string? culture) + public IRedirectUrl? GetMostRecentRedirectUrl(string url, string? culture) + { + if (string.IsNullOrWhiteSpace(culture)) { - if (string.IsNullOrWhiteSpace(culture)) return GetMostRecentRedirectUrl(url); - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetMostRecentUrl(url, 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 966e4ec7df96..20cd72e7ccaf 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.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; + _relationRepository = relationRepository; + _relationTypeRepository = relationTypeRepository; + _auditRepository = auditRepository; + _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); + } - public RelationService(ICoreScopeProvider uowProvider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IEntityService entityService, - IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, IAuditRepository auditRepository) - : base(uowProvider, loggerFactory, eventMessagesFactory) + /// + public IRelation? GetById(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - _relationRepository = relationRepository; - _relationTypeRepository = relationTypeRepository; - _auditRepository = auditRepository; - _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); + return _relationRepository.Get(id); } + } - /// - public IRelation? GetById(int id) + /// + public IRelationType? GetRelationTypeById(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationRepository.Get(id); - } + return _relationTypeRepository.Get(id); } + } - /// - public IRelationType? GetRelationTypeById(int id) + /// + public IRelationType? GetRelationTypeById(Guid id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationTypeRepository.Get(id); - } + return _relationTypeRepository.Get(id); } + } + + /// + public IRelationType? GetRelationTypeByAlias(string alias) => GetRelationType(alias); - /// - public IRelationType? GetRelationTypeById(Guid id) + /// + public IEnumerable GetAllRelations(params int[] ids) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationTypeRepository.Get(id); - } + return _relationRepository.GetMany(ids); } + } - /// - public IRelationType? GetRelationTypeByAlias(string alias) => GetRelationType(alias); + /// + public IEnumerable GetAllRelationsByRelationType(IRelationType relationType) => + GetAllRelationsByRelationType(relationType.Id); - /// - public IEnumerable GetAllRelations(params int[] ids) + /// + public IEnumerable GetAllRelationsByRelationType(int relationTypeId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationRepository.GetMany(ids); - } + IQuery query = Query().Where(x => x.RelationTypeId == relationTypeId); + return _relationRepository.Get(query); } + } - /// - public IEnumerable? GetAllRelationsByRelationType(IRelationType relationType) + /// + public IEnumerable GetAllRelationTypes(params int[] ids) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - return GetAllRelationsByRelationType(relationType.Id); + return _relationTypeRepository.GetMany(ids); } + } - /// - 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 GetByParentId(int id) => GetByParentId(id, null); - /// - public IEnumerable GetAllRelationTypes(params int[] ids) + /// + public IEnumerable GetByParentId(int id, string? relationTypeAlias) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + if (relationTypeAlias.IsNullOrWhiteSpace()) { - return _relationTypeRepository.GetMany(ids); + IQuery qry1 = Query().Where(x => x.ParentId == id); + return _relationRepository.Get(qry1); } - } - - /// - public IEnumerable? GetByParentId(int id) => GetByParentId(id, null); - /// - public IEnumerable GetByParentId(int id, string? relationTypeAlias) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + IRelationType? relationType = GetRelationType(relationTypeAlias!); + if (relationType == null) { - 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(); + 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) => GetByParentId(parent.Id); - /// - public IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias) => GetByParentId(parent.Id, relationTypeAlias); + /// + public IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias) => + GetByParentId(parent.Id, relationTypeAlias); - /// - public IEnumerable GetByChildId(int id) => GetByChildId(id, null); + /// + public IEnumerable GetByChildId(int id) => GetByChildId(id, null); - /// - public IEnumerable GetByChildId(int id, string? relationTypeAlias) + /// + public IEnumerable GetByChildId(int id, string? relationTypeAlias) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + if (relationTypeAlias.IsNullOrWhiteSpace()) { - 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(); + IQuery qry1 = Query().Where(x => x.ChildId == id); + return _relationRepository.Get(qry1); } - } - - /// - 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)) + IRelationType? relationType = GetRelationType(relationTypeAlias!); + if (relationType == null) { - var query = Query().Where(x => x.ChildId == id || x.ParentId == id); - return _relationRepository.Get(query); + return Enumerable.Empty(); } + + IQuery qry2 = + Query().Where(x => x.ChildId == id && x.RelationTypeId == relationType.Id); + return _relationRepository.Get(qry2); } + } - public IEnumerable GetByParentOrChildId(int id, string relationTypeAlias) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var relationType = GetRelationType(relationTypeAlias); - if (relationType == null) - return Enumerable.Empty(); + /// + public IEnumerable GetByChild(IUmbracoEntity child) => GetByChildId(child.Id); - var query = Query().Where(x => (x.ChildId == id || x.ParentId == id) && x.RelationTypeId == relationType.Id); - return _relationRepository.Get(query); - } - } + /// + public IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias) => + GetByChildId(child.Id, relationTypeAlias); - /// - public IRelation? GetByParentAndChildId(int parentId, int childId, IRelationType relationType) + /// + public IEnumerable GetByParentOrChildId(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - 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(); - } + IQuery query = Query().Where(x => x.ChildId == id || x.ParentId == id); + return _relationRepository.Get(query); } + } - /// - public IEnumerable GetByRelationTypeName(string relationTypeName) + public IEnumerable GetByParentOrChildId(int id, string relationTypeAlias) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - List? relationTypeIds; - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + IRelationType? relationType = GetRelationType(relationTypeAlias); + if (relationType == null) { - //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 Enumerable.Empty(); } - return relationTypeIds is null || relationTypeIds.Count == 0 - ? Enumerable.Empty() - : GetRelationsByListOfTypeIds(relationTypeIds); + IQuery query = Query().Where(x => + (x.ChildId == id || x.ParentId == id) && x.RelationTypeId == relationType.Id); + return _relationRepository.Get(query); } + } - /// - public IEnumerable GetByRelationTypeAlias(string relationTypeAlias) + /// + public IRelation? GetByParentAndChildId(int parentId, int childId, IRelationType relationType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - var relationType = GetRelationType(relationTypeAlias); - - return relationType == null - ? Enumerable.Empty() - : GetRelationsByListOfTypeIds(new[] { relationType.Id }); + IQuery query = Query().Where(x => x.ParentId == parentId && + x.ChildId == childId && + x.RelationTypeId == relationType.Id); + return _relationRepository.Get(query).FirstOrDefault(); } + } - /// - public IEnumerable? GetByRelationTypeId(int relationTypeId) + /// + public IEnumerable GetByRelationTypeName(string relationTypeName) + { + List? relationTypeIds; + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.RelationTypeId == relationTypeId); - return _relationRepository.Get(query); - } + // 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(); } - /// - 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); - } - } + return relationTypeIds.Count == 0 + ? Enumerable.Empty() + : GetRelationsByListOfTypeIds(relationTypeIds); + } + + /// + public IEnumerable GetByRelationTypeAlias(string relationTypeAlias) + { + IRelationType? relationType = GetRelationType(relationTypeAlias); - /// - public IUmbracoEntity? GetChildEntityFromRelation(IRelation relation) + return relationType == null + ? Enumerable.Empty() + : GetRelationsByListOfTypeIds(new[] { relationType.Id }); + } + + /// + public IEnumerable GetByRelationTypeId(int relationTypeId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); - return _entityService.Get(relation.ChildId, objectType); + IQuery query = Query().Where(x => x.RelationTypeId == relationTypeId); + return _relationRepository.Get(query); } + } - /// - public IUmbracoEntity? GetParentEntityFromRelation(IRelation relation) + /// + public IEnumerable GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering = null) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); - return _entityService.Get(relation.ParentId, objectType); + IQuery? query = Query().Where(x => x.RelationTypeId == relationTypeId); + return _relationRepository.GetPagedRelationsByQuery(query, pageIndex, pageSize, out totalRecords, ordering); } + } - /// - public Tuple? GetEntitiesFromRelation(IRelation relation) - { - var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); - var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); + /// + public IUmbracoEntity? GetChildEntityFromRelation(IRelation relation) + { + UmbracoObjectTypes objectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); + return _entityService.Get(relation.ChildId, objectType); + } - var child = _entityService.Get(relation.ChildId, childObjectType); - var parent = _entityService.Get(relation.ParentId, parentObjectType); + /// + public IUmbracoEntity? GetParentEntityFromRelation(IRelation relation) + { + UmbracoObjectTypes objectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); + return _entityService.Get(relation.ParentId, objectType); + } - if (parent is null || child is null) - { - return null; - } + /// + public Tuple? GetEntitiesFromRelation(IRelation relation) + { + UmbracoObjectTypes childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); + UmbracoObjectTypes parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); - return new Tuple(parent, child); - } + IEntitySlim? child = _entityService.Get(relation.ChildId, childObjectType); + IEntitySlim? parent = _entityService.Get(relation.ParentId, parentObjectType); - /// - public IEnumerable GetChildEntitiesFromRelations(IEnumerable relations) + if (parent is null || child is null) { - // 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; - } + return null; } - /// - 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 + return new Tuple(parent, child); + } - foreach (var groupedRelations in relations.GroupBy(x => ObjectTypes.GetUmbracoObjectType(x.ParentObjectType))) + /// + 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)) { - var objectType = groupedRelations.Key; - var ids = groupedRelations.Select(x => x.ParentId).ToArray(); - foreach (var e in _entityService.GetAll(objectType, ids)) - yield return e; + yield return e; } } + } - /// - public IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) + /// + 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))) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + UmbracoObjectTypes objectType = groupedRelations.Key; + var ids = groupedRelations.Select(x => x.ParentId).ToArray(); + foreach (IEntitySlim e in _entityService.GetAll(objectType, ids)) { - return _relationRepository.GetPagedParentEntitiesByChildId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); + yield return e; } } + } - /// - public IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) + /// + public IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationRepository.GetPagedChildEntitiesByParentId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); - } + return _relationRepository.GetPagedParentEntitiesByChildId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); } + } - /// - public IEnumerable> GetEntitiesFromRelations(IEnumerable relations) + /// + public IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - //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); - } - } + return _relationRepository.GetPagedChildEntitiesByParentId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); } + } - /// - public IRelation Relate(int parentId, int childId, IRelationType relationType) + /// + public IEnumerable> GetEntitiesFromRelations(IEnumerable relations) + { + // TODO: Argh! N+1 + foreach (IRelation relation in relations) { - // 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? + UmbracoObjectTypes childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); + UmbracoObjectTypes parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); - var relation = new Relation(parentId, childId, relationType); + IEntitySlim? child = _entityService.Get(relation.ChildId, childObjectType); + IEntitySlim? parent = _entityService.Get(relation.ParentId, parentObjectType); - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + if (parent is not null && child is not null) { - 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; + yield return new Tuple(parent, child); } } + } - /// - public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType) + /// + 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) { - return Relate(parent.Id, child.Id, relationType); + Save(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); - } + // 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); - /// - public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - 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)) + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new RelationSavingNotification(relation, eventMessages); + if (scope.Notifications.PublishCancelable(savingNotification)) { - var query = Query().Where(x => x.RelationTypeId == relationType.Id); - return _relationRepository.Get(query)?.Any() ?? false; + 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 bool IsRelated(int id) + /// + public IRelation Relate(int parentId, int childId, string relationTypeAlias) + { + IRelationType? relationType = GetRelationTypeByAlias(relationTypeAlias); + if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.ParentId == id || x.ChildId == id); - return _relationRepository.Get(query)?.Any() ?? false; - } + throw new ArgumentNullException( + string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); } - /// - public bool AreRelated(int parentId, int childId) + 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)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.ParentId == parentId && x.ChildId == childId); - return _relationRepository.Get(query)?.Any() ?? false; - } + throw new ArgumentNullException( + string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); } - /// - public bool AreRelated(int parentId, int childId, string relationTypeAlias) - { - var relType = GetRelationTypeByAlias(relationTypeAlias); - if (relType == null) - return false; + return Relate(parent.Id, child.Id, relationType); + } - return AreRelated(parentId, childId, relType); + /// + 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 AreRelated(int parentId, int childId, IRelationType relationType) + /// + public bool IsRelated(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - 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; - } + IQuery query = Query().Where(x => x.ParentId == id || x.ChildId == id); + return _relationRepository.Get(query).Any(); } + } - /// - public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child) + /// + public bool AreRelated(int parentId, int childId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - return AreRelated(parent.Id, child.Id); + IQuery query = Query().Where(x => x.ParentId == parentId && x.ChildId == childId); + return _relationRepository.Get(query).Any(); } + } - /// - public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) + /// + public bool AreRelated(int parentId, int childId, string relationTypeAlias) + { + IRelationType? relType = GetRelationTypeByAlias(relationTypeAlias); + if (relType == null) { - return AreRelated(parent.Id, child.Id, relationTypeAlias); + return false; } + return AreRelated(parentId, childId, relType); + } + + /// + public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child) => AreRelated(parent.Id, child.Id); - /// - public void Save(IRelation relation) + /// + 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()) { - using (var scope = ScopeProvider.CreateCoreScope()) + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new RelationSavingNotification(relation, eventMessages); + if (scope.Notifications.PublishCancelable(savingNotification)) { - 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)); + return; } + + _relationRepository.Save(relation); + scope.Complete(); + scope.Notifications.Publish( + new RelationSavedNotification(relation, eventMessages).WithStateFrom(savingNotification)); } + } - public void Save(IEnumerable relations) + public void Save(IEnumerable relations) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - IRelation[] relationsA = relations.ToArray(); + 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); + EventMessages messages = EventMessagesFactory.Get(); + var savingNotification = new RelationSavingNotification(relationsA, messages); + if (scope.Notifications.PublishCancelable(savingNotification)) + { scope.Complete(); - scope.Notifications.Publish(new RelationSavedNotification(relationsA, messages).WithStateFrom(savingNotification)); + return; } + + _relationRepository.Save(relationsA); + scope.Complete(); + scope.Notifications.Publish( + new RelationSavedNotification(relationsA, messages).WithStateFrom(savingNotification)); } + } - /// - public void Save(IRelationType relationType) + /// + public void Save(IRelationType relationType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new RelationTypeSavingNotification(relationType, eventMessages); + if (scope.Notifications.PublishCancelable(savingNotification)) { - 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)); + 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) + /// + public void Delete(IRelation relation) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + EventMessages eventMessages = EventMessagesFactory.Get(); + var deletingNotification = new RelationDeletingNotification(relation, eventMessages); + if (scope.Notifications.PublishCancelable(deletingNotification)) { - 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)); + return; } + + _relationRepository.Delete(relation); + scope.Complete(); + scope.Notifications.Publish( + new RelationDeletedNotification(relation, eventMessages).WithStateFrom(deletingNotification)); } + } - /// - public void Delete(IRelationType relationType) + /// + public void Delete(IRelationType relationType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + EventMessages eventMessages = EventMessagesFactory.Get(); + var deletingNotification = new RelationTypeDeletingNotification(relationType, eventMessages); + if (scope.Notifications.PublishCancelable(deletingNotification)) { - 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)); + return; } + + _relationTypeRepository.Delete(relationType); + scope.Complete(); + scope.Notifications.Publish( + new RelationTypeDeletedNotification(relationType, eventMessages).WithStateFrom(deletingNotification)); } + } - /// - public void DeleteRelationsOfType(IRelationType relationType) + /// + public void DeleteRelationsOfType(IRelationType relationType) + { + var relations = new List(); + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - 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); - } + 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 + // TODO: N+1, we should be able to do this in a single call + foreach (IRelation relation in relations) + { + _relationRepository.Delete(relation); + } - foreach (IRelation relation in relations) - { - _relationRepository.Delete(relation); - } + scope.Complete(); - scope.Complete(); + scope.Notifications.Publish(new RelationDeletedNotification(relations, EventMessagesFactory.Get())); + } + } - 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 + #region Private Methods - private IRelationType? GetRelationType(string relationTypeAlias) + private IRelationType? GetRelationType(string relationTypeAlias) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.Alias == relationTypeAlias); - return _relationTypeRepository.Get(query)?.FirstOrDefault(); - } + IQuery query = Query().Where(x => x.Alias == relationTypeAlias); + return _relationTypeRepository.Get(query).FirstOrDefault(); } + } - private IEnumerable GetRelationsByListOfTypeIds(IEnumerable relationTypeIds) + private IEnumerable GetRelationsByListOfTypeIds(IEnumerable relationTypeIds) + { + var relations = new List(); + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - var relations = new List(); - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + foreach (var relationTypeId in relationTypeIds) { - 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); - } - } + 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, ObjectTypes.GetName(UmbracoObjectTypes.RelationType), message)); - } - #endregion + 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 85e78672ee61..2c7bb39085fa 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; } + EventMessagesFactory = eventMessagesFactory ?? throw new ArgumentNullException(nameof(eventMessagesFactory)); + ScopeProvider = provider ?? throw new ArgumentNullException(nameof(provider)); + LoggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + } - protected ICoreScopeProvider ScopeProvider { get; } + protected IEventMessagesFactory EventMessagesFactory { get; } - protected ILoggerFactory LoggerFactory { get; } + protected ICoreScopeProvider ScopeProvider { 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 ILoggerFactory LoggerFactory { get; } - protected IQuery Query() => ScopeProvider.CreateQuery(); - } + protected IQuery Query() => ScopeProvider.CreateQuery(); } diff --git a/src/Umbraco.Core/Services/SectionService.cs b/src/Umbraco.Core/Services/SectionService.cs index b698579b653e..61ff97889483 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 IUserService _userService; - private readonly SectionCollection _sectionCollection; + private readonly SectionCollection _sectionCollection; + private readonly IUserService _userService; - public SectionService( - IUserService userService, - SectionCollection sectionCollection) - { - _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _sectionCollection = sectionCollection ?? throw new ArgumentNullException(nameof(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; + /// + /// The cache storage for all applications + /// + public IEnumerable GetSections() + => _sectionCollection; - /// - public IEnumerable GetAllowedSections(int userId) + /// + public IEnumerable GetAllowedSections(int userId) + { + IUser? user = _userService.GetUserById(userId); + if (user == null) { - 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)); + throw new InvalidOperationException("No user found with id " + userId); } - /// - public ISection? GetByAlias(string appAlias) - => GetSections().FirstOrDefault(t => t.Alias.Equals(appAlias, StringComparison.OrdinalIgnoreCase)); + 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 c92977aab0c7..070e9e8e1f52 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.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; - } + _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) + /// + /// 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()) { - var serverIdentity = GetCurrentServerIdentity(); - using (var scope = ScopeProvider.CreateCoreScope()) - { - scope.WriteLock(Cms.Core.Constants.Locks.Servers); - - _serverRegistrationRepository.ClearCache(); // ensure we have up-to-date cache + scope.WriteLock(Constants.Locks.Servers); - var regs = _serverRegistrationRepository.GetMany()?.ToArray(); - var hasSchedulingPublisher = regs?.Any(x => ((ServerRegistration) x).IsSchedulingPublisher); - var server = regs?.FirstOrDefault(x => x.ServerIdentity?.InvariantEquals(serverIdentity) ?? false); + _serverRegistrationRepository.ClearCache(); // ensure we have up-to-date cache - 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; - } + IServerRegistration[]? regs = _serverRegistrationRepository.GetMany()?.ToArray(); + var hasSchedulingPublisher = regs?.Any(x => ((ServerRegistration)x).IsSchedulingPublisher); + IServerRegistration? server = + regs?.FirstOrDefault(x => x.ServerIdentity?.InvariantEquals(serverIdentity) ?? false); - server.IsActive = true; - if (hasSchedulingPublisher == false) - server.IsSchedulingPublisher = true; + 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; + } - _serverRegistrationRepository.Save(server); - _serverRegistrationRepository.DeactiveStaleServers(staleTimeout); // triggers a cache reload + server.IsActive = true; + if (hasSchedulingPublisher == false) + { + server.IsSchedulingPublisher = true; + } - // reload - cheap, cached + _serverRegistrationRepository.Save(server); + _serverRegistrationRepository.DeactiveStaleServers(staleTimeout); // triggers a cache reload - regs = _serverRegistrationRepository.GetMany()?.ToArray(); + // 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; + // 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(); - } + scope.Complete(); } + } - /// - /// Deactivates a server. - /// - /// The server unique identity. - public void DeactiveServer(string serverIdentity) + /// + /// 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()) { - // because the repository caches "all" and has queries disabled... + scope.WriteLock(Constants.Locks.Servers); - 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 - _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; + } - 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 + server.IsActive = server.IsSchedulingPublisher = false; + _serverRegistrationRepository.Save(server); // will trigger a cache reload // will trigger a cache reload - scope.Complete(); - } + scope.Complete(); } + } - /// - /// Deactivates stale servers. - /// - /// The time after which a server is considered stale. - public void DeactiveStaleServers(TimeSpan staleTimeout) + /// + /// Deactivates stale servers. + /// + /// The time after which a server is considered stale. + public void DeactiveStaleServers(TimeSpan staleTimeout) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (var scope = ScopeProvider.CreateCoreScope()) - { - scope.WriteLock(Cms.Core.Constants.Locks.Servers); - _serverRegistrationRepository.DeactiveStaleServers(staleTimeout); - scope.Complete(); - } + 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 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) + /// + /// 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)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + scope.ReadLock(Constants.Locks.Servers); + if (refresh) { - scope.ReadLock(Cms.Core.Constants.Locks.Servers); - if (refresh) - { - _serverRegistrationRepository.ClearCache(); - } - - return _serverRegistrationRepository.GetMany().ToArray(); // fast, cached // fast, cached + _serverRegistrationRepository.ClearCache(); } - } - /// - /// 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; + 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 20774bd7a2ca..0e24f27be50c 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; - - /// - /// 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) - { - _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; - } + _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; + } - /// - /// 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); + /// + /// Gets the + /// + public IPublicAccessService? PublicAccessService => _publicAccessService?.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) - ); - } + /// + /// Gets the + /// + public IDomainService? DomainService => _domainService?.Value; - /// - /// Gets the - /// - public IPublicAccessService? PublicAccessService => _publicAccessService?.Value; + /// + /// Gets the + /// + public IAuditService? AuditService => _auditService?.Value; - /// - /// Gets the - /// - public IDomainService? DomainService => _domainService?.Value; + /// + /// Gets the + /// + public ILocalizedTextService? TextService => _localizedTextService?.Value; - /// - /// Gets the - /// - public IAuditService? AuditService => _auditService?.Value; + /// + /// Gets the + /// + public INotificationService? NotificationService => _notificationService?.Value; - /// - /// Gets the - /// - public ILocalizedTextService? TextService => _localizedTextService?.Value; + /// + /// Gets the + /// + public IServerRegistrationService? ServerRegistrationService => _serverRegistrationService?.Value; - /// - /// Gets the - /// - public INotificationService? NotificationService => _notificationService?.Value; + /// + /// Gets the + /// + public ITagService? TagService => _tagService?.Value; - /// - /// Gets the - /// - public IServerRegistrationService? ServerRegistrationService => _serverRegistrationService?.Value; + /// + /// Gets the + /// + public IMacroService? MacroService => _macroService?.Value; - /// - /// Gets the - /// - public ITagService? TagService => _tagService?.Value; + /// + /// Gets the + /// + public IEntityService? EntityService => _entityService?.Value; - /// - /// Gets the - /// - public IMacroService? MacroService => _macroService?.Value; + /// + /// Gets the + /// + public IRelationService? RelationService => _relationService?.Value; - /// - /// Gets the - /// - public IEntityService? EntityService => _entityService?.Value; + /// + /// Gets the + /// + public IContentService? ContentService => _contentService?.Value; - /// - /// Gets the - /// - public IRelationService? RelationService => _relationService?.Value; + /// + /// Gets the + /// + public IContentTypeService? ContentTypeService => _contentTypeService?.Value; - /// - /// Gets the - /// - public IContentService? ContentService => _contentService?.Value; + /// + /// Gets the + /// + public IMediaTypeService? MediaTypeService => _mediaTypeService?.Value; - /// - /// Gets the - /// - public IContentTypeService? ContentTypeService => _contentTypeService?.Value; + /// + /// Gets the + /// + public IDataTypeService? DataTypeService => _dataTypeService?.Value; - /// - /// Gets the - /// - public IMediaTypeService? MediaTypeService => _mediaTypeService?.Value; + /// + /// Gets the + /// + public IFileService? FileService => _fileService?.Value; - /// - /// Gets the - /// - public IDataTypeService? DataTypeService => _dataTypeService?.Value; + /// + /// Gets the + /// + public ILocalizationService? LocalizationService => _localizationService?.Value; - /// - /// Gets the - /// - public IFileService? FileService => _fileService?.Value; + /// + /// Gets the + /// + public IMediaService? MediaService => _mediaService?.Value; - /// - /// Gets the - /// - public ILocalizationService? LocalizationService => _localizationService?.Value; + /// + /// Gets the + /// + public IPackagingService? PackagingService => _packagingService?.Value; - /// - /// Gets the - /// - public IMediaService? MediaService => _mediaService?.Value; + /// + /// Gets the + /// + public IUserService? UserService => _userService?.Value; - /// - /// Gets the - /// - public IPackagingService? PackagingService => _packagingService?.Value; + /// + /// Gets the + /// + public IMemberService? MemberService => _memberService?.Value; - /// - /// Gets the - /// - public IUserService? UserService => _userService?.Value; + /// + /// Gets the MemberTypeService + /// + public IMemberTypeService? MemberTypeService => _memberTypeService?.Value; - /// - /// Gets the - /// - public IMemberService? MemberService => _memberService?.Value; + /// + /// Gets the MemberGroupService + /// + public IMemberGroupService? MemberGroupService => _memberGroupService?.Value; - /// - /// Gets the MemberTypeService - /// - public IMemberTypeService? MemberTypeService => _memberTypeService?.Value; + /// + /// Gets the ExternalLoginService. + /// + public IExternalLoginService? ExternalLoginService => _externalLoginService?.Value; - /// - /// Gets the MemberGroupService - /// - public IMemberGroupService? MemberGroupService => _memberGroupService?.Value; + /// + /// Gets the RedirectUrlService. + /// + public IRedirectUrlService? RedirectUrlService => _redirectUrlService?.Value; - /// - /// Gets the ExternalLoginService. - /// - public IExternalLoginService? ExternalLoginService => _externalLoginService?.Value; + /// + /// Gets the ConsentService. + /// + public IConsentService? ConsentService => _consentService?.Value; - /// - /// Gets the RedirectUrlService. - /// - public IRedirectUrlService? RedirectUrlService => _redirectUrlService?.Value; + /// + /// Gets the KeyValueService. + /// + public IKeyValueService? KeyValueService => _keyValueService?.Value; - /// - /// Gets the ConsentService. - /// - public IConsentService? ConsentService => _consentService?.Value; + /// + /// Gets the ContentTypeServiceBaseFactory. + /// + public IContentTypeBaseServiceProvider? ContentTypeBaseServices => _contentTypeBaseServiceProvider?.Value; - /// - /// Gets the KeyValueService. - /// - public IKeyValueService? KeyValueService => _keyValueService?.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) + { + return service == null ? null : new Lazy(() => service); + } - /// - /// 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 65e4a32f9ed9..c75863f6de67 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; + private readonly ITagRepository _tagRepository; - public TagService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, - ITagRepository tagRepository) - : base(provider, loggerFactory, eventMessagesFactory) - { - _tagRepository = tagRepository; - } + public TagService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, ITagRepository tagRepository) + : base(provider, loggerFactory, eventMessagesFactory) => + _tagRepository = tagRepository; - /// - public TaggedEntity? GetTaggedEntityById(int id) + /// + public TaggedEntity? GetTaggedEntityById(int id) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntityById(id); - } + return _tagRepository.GetTaggedEntityById(id); } + } - /// - public TaggedEntity? GetTaggedEntityByKey(Guid key) + /// + public TaggedEntity? GetTaggedEntityByKey(Guid key) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntityByKey(key); - } + return _tagRepository.GetTaggedEntityByKey(key); } + } - /// - public IEnumerable GetTaggedContentByTagGroup(string group, string? culture = null) + /// + public IEnumerable GetTaggedContentByTagGroup(string group, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, group, culture); } + } - /// - public IEnumerable GetTaggedContentByTag(string tag, string? group = null, 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.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, tag, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, tag, group, culture); } + } - /// - public IEnumerable GetTaggedMediaByTagGroup(string group, string? culture = null) + /// + public IEnumerable GetTaggedMediaByTagGroup(string group, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Media, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Media, group, culture); } + } - /// - public IEnumerable GetTaggedMediaByTag(string tag, string? group = null, 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.GetTaggedEntitiesByTag(TaggableObjectTypes.Media, tag, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Media, tag, group, culture); } + } - /// - public IEnumerable GetTaggedMembersByTagGroup(string group, string? culture = null) + /// + public IEnumerable GetTaggedMembersByTagGroup(string group, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Member, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Member, group, culture); } + } - /// - public IEnumerable GetTaggedMembersByTag(string tag, string? group = null, 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.GetTaggedEntitiesByTag(TaggableObjectTypes.Member, tag, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Member, tag, group, culture); } + } - /// - public IEnumerable GetAllTags(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.GetTagsForEntityType(TaggableObjectTypes.All, group, culture); - } + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.All, group, culture); } + } - /// - public IEnumerable GetAllContentTags(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.Content, group, culture); - } + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Content, group, culture); } + } - /// - public IEnumerable GetAllMediaTags(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.Media, group, culture); - } + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Media, group, culture); } + } - /// - public IEnumerable GetAllMemberTags(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.Member, group, culture); - } + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Member, group, culture); } + } - /// - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, 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.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); - } + return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); } + } - /// - public IEnumerable GetTagsForEntity(int contentId, 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.GetTagsForEntity(contentId, group, culture); - } + return _tagRepository.GetTagsForEntity(contentId, group, culture); } + } - /// - public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, 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.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); - } + return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); } + } - /// - public IEnumerable GetTagsForEntity(Guid contentId, 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.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 ab5a09ce8ba8..32dc9c18cc56 100644 --- a/src/Umbraco.Core/Services/TrackedReferencesService.cs +++ b/src/Umbraco.Core/Services/TrackedReferencesService.cs @@ -2,58 +2,60 @@ 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) + { + _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); + + 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); + + 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) { - private readonly ITrackedReferencesRepository _trackedReferencesRepository; - private readonly ICoreScopeProvider _scopeProvider; - private readonly IEntityService _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); - var items = _trackedReferencesRepository.GetPagedRelationsForItem(id, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); - - 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); - var items = _trackedReferencesRepository.GetPagedItemsWithRelations(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); - - 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); - - var items = _trackedReferencesRepository.GetPagedDescendantsInReferences( - parentId, - pageIndex, - pageSize, - filterMustBeIsDependency, - out var totalItems); - return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; - } + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + + 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 f325712d77ce..3b2b5f361814 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; - - /// - /// 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 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)); - - /// - 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 IDictionary> GetBySectionGrouped(string sectionAlias, TreeUse use = TreeUse.Main) - { - return GetBySection(sectionAlias, use).GroupBy(x => x.TreeGroup).ToDictionary( - x => x.Key ?? "", - x => (IEnumerable) x.ToArray()); - } - } + /// + public TreeService(TreeCollection treeCollection) => _treeCollection = treeCollection; + + /// + public Tree? GetByAlias(string treeAlias) => _treeCollection.FirstOrDefault(x => x.TreeAlias == treeAlias); + + /// + 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)); + + /// + 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 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 7a4feb91fbdc..de79284ac9fc 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.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; - - /// - /// 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); - } + _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); - /// - public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) + if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey); + throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); } - /// - public async Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey) + var isValid = secret is not null && generator.ValidateTwoFactorPIN(secret, code); + + if (!isValid) { - return await GetEnabledProviderNamesAsync(userOrMemberKey); + return false; } - public async Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code) - { - var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); + return await DisableAsync(userOrMemberKey, providerName); + } - if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) + public async Task ValidateAndSaveAsync(string providerName, Guid userOrMemberKey, string secret, string code) + { + try + { + var isValid = ValidateTwoFactorSetup(providerName, secret, code); + if (isValid == false) { - throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); + return false; } - var isValid = secret is not null && generator.ValidateTwoFactorPIN(secret, code); - - if (!isValid) + var twoFactorLogin = new TwoFactorLogin { - return false; - } + Confirmed = true, + Secret = secret, + UserOrMemberKey = userOrMemberKey, + ProviderName = providerName, + }; - return await DisableAsync(userOrMemberKey, providerName); - } + await SaveAsync(twoFactorLogin); - public async Task ValidateAndSaveAsync(string providerName, Guid userOrMemberKey, string secret, string code) + return true; + } + catch (Exception ex) { - - try - { - var isValid = ValidateTwoFactorSetup(providerName, secret, code); - if (isValid == false) - { - return false; - } - - 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"); - } - - return false; + _logger.LogError(ex, "Could not log in with the provided one-time-password"); } - 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 false; + } - return providersOnUser.Where(IsKnownProviderName)!; - } + /// + public async Task IsTwoFactorEnabledAsync(Guid userOrMemberKey) => + (await GetEnabledProviderNamesAsync(userOrMemberKey)).Any(); - /// - /// 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; - } - if (_identityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) - { - return true; - } + /// + 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; + } - if (_backOfficeIdentityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) - { - return true; - } + /// + public async Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName) + { + var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); - return false; + // Dont allow to generate a new secrets if user already has one + if (!string.IsNullOrEmpty(secret)) + { + return default; } - /// - public async Task IsTwoFactorEnabledAsync(Guid userOrMemberKey) + secret = GenerateSecret(); + + if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) { - return (await GetEnabledProviderNamesAsync(userOrMemberKey)).Any(); + throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); } - /// - public async Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string 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)) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - return (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)).FirstOrDefault(x => x.ProviderName == providerName)?.Secret; + throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); } - /// - public async Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName) - { - var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); + return generator.ValidateTwoFactorSetup(secret, code); + } - // Dont allow to generate a new secrets if user already has one - if (!string.IsNullOrEmpty(secret)) - { - return default; - } + /// + public Task SaveAsync(TwoFactorLogin twoFactorLogin) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + _twoFactorLoginRepository.Save(twoFactorLogin); - secret = GenerateSecret(); + return Task.CompletedTask; + } - if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) - { - throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); - } + /// + /// Generates a new random unique secret. + /// + /// The random secret + protected virtual string GenerateSecret() => Guid.NewGuid().ToString(); - return await generator.GetSetupDataAsync(userOrMemberKey, secret); - } + private async Task> GetEnabledProviderNamesAsync(Guid userOrMemberKey) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + var providersOnUser = (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)) + .Select(x => x.ProviderName).ToArray(); - /// - public IEnumerable GetAllProviderNames() => _twoFactorSetupGenerators.Keys; + return providersOnUser.Where(IsKnownProviderName); + } - /// - public async Task DisableAsync(Guid userOrMemberKey, string providerName) + /// + /// 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) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - return await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey, providerName); + return false; } - /// - public bool ValidateTwoFactorSetup(string providerName, string secret, string code) + if (_identityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) { - if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) - { - throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); - } - - return generator.ValidateTwoFactorSetup(secret, code); + return true; } - /// - public Task SaveAsync(TwoFactorLogin twoFactorLogin) + if (_backOfficeIdentityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - _twoFactorLoginRepository.Save(twoFactorLogin); - - return Task.CompletedTask; + return true; } - /// - /// 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 e2003f837082..7a5269d2bf67 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 a3c6bd11b466..14b2e581f9b1 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 IUmbracoVersion _version; - private readonly ILocalizationService _localizationService; + private readonly ILocalizationService _localizationService; + private readonly IUmbracoVersion _version; + public UserDataService(IUmbracoVersion version, ILocalizationService localizationService) + { + _version = version; + _localizationService = localizationService; + } - public UserDataService(IUmbracoVersion version, ILocalizationService localizationService) + public IEnumerable GetUserData() => + new List { - _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()) - }; + 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() + public bool IsRunningInProcessIIS() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return false; - } - - string processName = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName); - return (processName.Contains("w3wp") || processName.Contains("iisexpress")); + 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 f0b5cc6a327f..88e2708b2ce1 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.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 { + 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) + { + _runtimeState = runtimeState; + _userRepository = userRepository; + _userGroupRepository = userGroupRepository; + _globalSettings = globalSettings.Value; + _logger = loggerFactory.CreateLogger(); + } + + private bool IsUpgrading => + _runtimeState.Level == RuntimeLevel.Install || _runtimeState.Level == RuntimeLevel.Upgrade; + /// - /// Represents the UserService, which is an easy access to operations involving , and eventually Backoffice Users. + /// Checks in a set of permissions associated with a user for those related to a given nodeId /// - internal class UserService : RepositoryService, IUserService + /// 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) { - private readonly IRuntimeState _runtimeState; - private readonly IUserRepository _userRepository; - private readonly IUserGroupRepository _userGroupRepository; - private readonly GlobalSettings _globalSettings; - private readonly ILogger _logger; - - public UserService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IRuntimeState runtimeState, - IUserRepository userRepository, IUserGroupRepository userGroupRepository, IOptions globalSettings) - : base(provider, loggerFactory, eventMessagesFactory) + if (permissions.Any(x => x.EntityId == nodeId)) { - _runtimeState = runtimeState; - _userRepository = userRepository; - _userGroupRepository = userGroupRepository; - _globalSettings = globalSettings.Value; - _logger = loggerFactory.CreateLogger(); - } - - private bool IsUpgrading => _runtimeState.Level == RuntimeLevel.Install || _runtimeState.Level == RuntimeLevel.Upgrade; - - #region Implementation of IMembershipUserService + EntityPermission found = permissions.First(x => x.EntityId == nodeId); + var assignedPermissionsArray = found.AssignedPermissions.ToList(); - /// - /// 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); } - } - /// - /// 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) - { - return CreateUserWithIdentity(username, email, string.Empty); + assignedPermissions = string.Join(string.Empty, assignedPermissionsArray); + return true; } - /// - /// 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) - { - return CreateUserWithIdentity(username, email, passwordValue); - } + assignedPermissions = string.Empty; + return false; + } - /// - /// 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) - { - return CreateUserWithIdentity(username, email, passwordValue, isApproved); - } + #region Implementation of IMembershipUserService - /// - /// 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) + /// + /// 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)) { - 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) - throw new ArgumentException("Login already exists"); // causes rollback + return _userRepository.ExistsByUserName(username); + } + } - 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; - } + /// + /// 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); - _userRepository.Save(user); + /// + /// 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); - scope.Notifications.Publish(new UserSavedNotification(user, evtMsgs).WithStateFrom(savingNotification)); - scope.Complete(); - } + /// + /// 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); - return user; + /// + /// Gets a User by its integer id + /// + /// Id + /// + /// + /// + public IUser? GetById(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.Get(id); } + } - /// - /// Gets a User by its integer id - /// - /// Id - /// - public IUser? GetById(int 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) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.Get(id); - } + throw new ArgumentNullException(nameof(username)); } - /// - /// Gets an by its provider key - /// - /// Id to use for retrieval - /// - public IUser? GetByProviderKey(object id) + if (string.IsNullOrWhiteSpace(username)) { - var asInt = id.TryConvertTo(); - return asInt.Success ? GetById(asInt.Result) : null; + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(username)); } - /// - /// Get an by email - /// - /// Email to use for retrieval - /// - public IUser? GetByEmail(string email) + EventMessages evtMsgs = EventMessagesFactory.Get(); + + // TODO: PUT lock here!! + User user; + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + var loginExists = _userRepository.ExistsByLogin(username); + if (loginExists) { - var query = Query().Where(x => x.Email.Equals(email)); - return _userRepository.Get(query)?.FirstOrDefault(); + throw new ArgumentException("Login already exists"); // causes rollback } - } - /// - /// Get an by username - /// - /// Username to use for retrieval - /// - public IUser? GetByUsername(string? username) - { - if (username is null) + user = new User(_globalSettings) { - return null; + 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; } - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - try - { - 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); - } + _userRepository.Save(user); - throw; - } - } + scope.Notifications.Publish(new UserSavedNotification(user, evtMsgs).WithStateFrom(savingNotification)); + scope.Complete(); } - /// - /// Disables an - /// - /// to disable - public void Delete(IUser membershipUser) + 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)) { - //disable - membershipUser.IsApproved = false; + IQuery query = Query().Where(x => x.Email.Equals(email)); + return _userRepository.Get(query)?.FirstOrDefault(); + } + } - Save(membershipUser); + /// + /// Get an by username + /// + /// Username to use for retrieval + /// + /// + /// + public IUser? GetByUsername(string? username) + { + if (username is null) + { + return null; } - /// - /// 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) + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - if (deletePermanently == false) + try { - Delete(user); + return _userRepository.GetByUsername(username, true); } - else + catch (DbException) { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) + // 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) { - 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(); + // NOTE: this will not be cached + return _userRepository.GetByUsername(username, false); } + + throw; } } + } - // 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) + /// + /// 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) { - _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."); + Delete(user); } - - /// - /// Saves an - /// - /// to Save - public void Save(IUser entity) + else { - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); - using (var scope = ScopeProvider.CreateCoreScope()) + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - var savingNotification = new UserSavingNotification(entity, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) + var deletingNotification = new UserDeletingNotification(user, evtMsgs); + if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); return; } - if (string.IsNullOrWhiteSpace(entity.Username)) - throw new ArgumentException("Empty username.", nameof(entity)); + _userRepository.Delete(user); - 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(); - } + scope.Notifications.Publish( + new UserDeletedNotification(user, evtMsgs).WithStateFrom(deletingNotification)); + scope.Complete(); } } + } - /// - /// Saves a list of objects - /// - /// to save - public void Save(IEnumerable entities) - { - var evtMsgs = EventMessagesFactory.Get(); + // 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."); - var entitiesA = entities.ToArray(); + /// + /// Saves an + /// + /// to Save + public void Save(IUser entity) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); - using (var scope = ScopeProvider.CreateCoreScope()) + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var savingNotification = new UserSavingNotification(entity, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) { - var savingNotification = new UserSavingNotification(entitiesA, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return; - } + scope.Complete(); + return; + } - foreach (var user in entitiesA) - { - if (string.IsNullOrWhiteSpace(user.Username)) - throw new ArgumentException("Empty username.", nameof(entities)); + if (string.IsNullOrWhiteSpace(entity.Username)) + { + throw new ArgumentException("Empty username.", nameof(entity)); + } - if (string.IsNullOrWhiteSpace(user.Name)) - throw new ArgumentException("Empty name.", nameof(entities)); + if (string.IsNullOrWhiteSpace(entity.Name)) + { + throw new ArgumentException("Empty name.", nameof(entity)); + } - _userRepository.Save(user); + 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; } - scope.Notifications.Publish(new UserSavedNotification(entitiesA, evtMsgs).WithStateFrom(savingNotification)); + _logger.LogWarning( + ex, + "An error occurred attempting to save a user instance during upgrade, normally this warning can be ignored"); - //commit the whole lot in one go + // we don't want the uow to rollback its scope! scope.Complete(); } } + } - /// - /// This is just the default user group that the membership provider will use - /// - /// - public string GetDefaultMemberType() - { - return Cms.Core.Constants.Security.WriterGroupAlias; - } + /// + /// Saves a list of objects + /// + /// to save + public void Save(IEnumerable entities) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + IUser[] entitiesA = entities.ToArray(); - /// - /// 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()) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + var savingNotification = new UserSavingNotification(entitiesA, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) { - 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); + scope.Complete(); + return; } - } - /// - /// 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)) + foreach (IUser user in entitiesA) { - var query = Query(); - - switch (matchType) + if (string.IsNullOrWhiteSpace(user.Username)) { - 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)); + throw new ArgumentException("Empty username.", nameof(entities)); } - 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) + if (string.IsNullOrWhiteSpace(user.Name)) { - 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)); + throw new ArgumentException("Empty name.", nameof(entities)); } - return _userRepository.GetCountByQuery(query); + _userRepository.Save(user); } + + scope.Notifications.Publish( + new UserSavedNotification(entitiesA, evtMsgs).WithStateFrom(savingNotification)); + + // commit the whole lot in one go + scope.Complete(); } + } - public Guid CreateLoginSession(int userId, string requestingIpAddress) + /// + /// 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)) { - using (var scope = ScopeProvider.CreateCoreScope()) + IQuery query = Query(); + + switch (matchType) { - var session = _userRepository.CreateLoginSession(userId, requestingIpAddress); - scope.Complete(); - return session; + 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); } + } - public int ClearLoginSessions(int userId) + /// + /// 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)) { - using (var scope = ScopeProvider.CreateCoreScope()) + IQuery query = Query(); + + switch (matchType) { - var count = _userRepository.ClearLoginSessions(userId); - scope.Complete(); - return count; + 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); } + } - public void ClearLoginSession(Guid sessionId) + /// + /// 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)) { - using (var scope = ScopeProvider.CreateCoreScope()) + IQuery? query; + + switch (countType) { - _userRepository.ClearLoginSession(sessionId); - scope.Complete(); + 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 bool ValidateLoginSession(int userId, Guid sessionId) + public Guid CreateLoginSession(int userId, string requestingIpAddress) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - var result = _userRepository.ValidateLoginSession(userId, sessionId); - scope.Complete(); - return result; - } + Guid session = _userRepository.CreateLoginSession(userId, requestingIpAddress); + scope.Complete(); + return session; } + } - public IDictionary GetUserStates() + public int ClearLoginSessions(int userId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetUserStates(); - } + var count = _userRepository.ClearLoginSessions(userId); + scope.Complete(); + return count; } + } - public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState[]? userState = null, string[]? userGroups = null, string? filter = null) + public void ClearLoginSession(Guid sessionId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - IQuery? filterQuery = null; - if (filter.IsNullOrWhiteSpace() == false) - { - filterQuery = Query()?.Where(x => (x.Name != null && x.Name.Contains(filter!)) || x.Username.Contains(filter!)); - } + _userRepository.ClearLoginSession(sessionId); + scope.Complete(); + } + } - return GetAll(pageIndex, pageSize, out totalRecords, orderBy, orderDirection, userState, userGroups, null, filterQuery); + public bool ValidateLoginSession(int userId, Guid sessionId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var result = _userRepository.ValidateLoginSession(userId, sessionId); + scope.Complete(); + return result; } + } - 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) + public IDictionary GetUserStates() + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - 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.GetUserStates(); + } + } - return _userRepository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, sort, orderDirection, includeUserGroups, excludeUserGroups, userState, filter); - } + 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!)); } - /// - /// 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) + 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)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + Expression> sort; + switch (orderBy.ToUpperInvariant()) { - return _userRepository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, member => member.Name); + 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); } + } - public IEnumerable GetNextUsers(int id, int count) + /// + /// 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)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetNextUsers(id, count); - } + return _userRepository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, member => member.Name); } + } - /// - /// Gets a list of objects associated with a given group - /// - /// Id of group - /// - public IEnumerable GetAllInGroup(int? groupId) + public IEnumerable GetNextUsers(int id, int count) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - if (groupId is null) - { - return Array.Empty(); - } + return _userRepository.GetNextUsers(id, count); + } + } - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetAllInGroup(groupId.Value); - } + /// + /// 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(); } - /// - /// Gets a list of objects not associated with a given group - /// - /// Id of group - /// - public IEnumerable GetAllNotInGroup(int groupId) + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope()) - { - return _userRepository.GetAllNotInGroup(groupId); - } + return _userRepository.GetAllInGroup(groupId.Value); } + } - #endregion + /// + /// 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); + } + } - #region Implementation of IUserService + #endregion - /// - /// Gets an IProfile by User Id. - /// - /// Id of the User to retrieve - /// - public IProfile? GetProfileById(int id) + #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) { - //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); + return null; } - /// - /// Gets a profile by username - /// - /// Username - /// - public IProfile? GetProfileByUserName(string username) + 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)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetProfile(username); - } + return _userRepository.GetProfile(username); } + } - /// - /// Gets a user by Id - /// - /// Id of the user to retrieve - /// - public IUser? GetUserById(int id) + /// + /// Gets a user by Id + /// + /// Id of the user to retrieve + /// + /// + /// + public IUser? GetUserById(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + try { - 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) { - return _userRepository.Get(id); + // NOTE: this will not be cached + return _userRepository.Get(id, false); } - 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; - } + throw; } } + } - public IEnumerable GetUsersById(params int[]? ids) + public IEnumerable GetUsersById(params int[]? ids) + { + if (ids?.Length <= 0) { - if (ids?.Length <= 0) return Enumerable.Empty(); - - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetMany(ids); - } + return Enumerable.Empty(); } - /// - /// 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) + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - 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)); - } - } + return _userRepository.GetMany(ids); } + } - /// - /// 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) + /// + /// 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) { - if (entityIds.Length == 0) - return; + return; + } - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); - using (var scope = ScopeProvider.CreateCoreScope()) - { - _userGroupRepository.AssignGroupPermission(groupId, permission, entityIds); - scope.Complete(); + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _userGroupRepository.ReplaceGroupPermissions(groupId, permissions, entityIds); + scope.Complete(); - var assigned = new[] { permission.ToString(CultureInfo.InvariantCulture) }; - var entityPermissions = entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); + 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)); } } + } - /// - /// Gets all UserGroups or those specified as parameters - /// - /// Optional Ids of UserGroups to retrieve - /// An enumerable list of - public IEnumerable GetAllUserGroups(params int[] ids) + /// + /// 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) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userGroupRepository.GetMany(ids).OrderBy(x => x.Name); - } + return; } - public IEnumerable GetUserGroupsByAlias(params string[] aliases) + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - if (aliases.Length == 0) return Enumerable.Empty(); + _userGroupRepository.AssignGroupPermission(groupId, permission, entityIds); + scope.Complete(); - 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(); - } + 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 a UserGroup by its Alias - /// - /// Alias of the UserGroup to retrieve - /// - public IUserGroup? GetUserGroupByAlias(string alias) + /// + /// 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)) { - 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(); - } + return _userGroupRepository.GetMany(ids).OrderBy(x => x.Name); } + } - /// - /// Gets a UserGroup by its Id - /// - /// Id of the UserGroup to retrieve - /// - public IUserGroup? GetUserGroupById(int id) + public IEnumerable GetUserGroupsByAlias(params string[] aliases) + { + if (aliases.Length == 0) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userGroupRepository.Get(id); - } + return Enumerable.Empty(); } - /// - /// 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) + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - 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; + IQuery query = Query().Where(x => aliases.SqlIn(x.Alias)); + IEnumerable contents = _userGroupRepository.Get(query); + return contents?.WhereNotNull().ToArray() ?? Enumerable.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); + /// + /// 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"); + } - 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(); - } + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.Alias == alias); + IEnumerable contents = _userGroupRepository.Get(query); + return contents?.FirstOrDefault(); + } + } - var userGroupWithUsers = new UserGroupWithUsers(userGroup, addedUsers, removedUsers); + /// + /// 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); + } + } - // 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; - } + /// + /// 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(); - // this is an additional notification for special auditing - var savingUserGroupWithUsersNotification = new UserGroupWithUsersSavingNotification(userGroupWithUsers, evtMsgs); - if (scope.Notifications.PublishCancelable(savingUserGroupWithUsersNotification)) - { - scope.Complete(); - return; - } + 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; - _userGroupRepository.AddOrUpdateGroupWithUsers(userGroup, userIds); + 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(); + } - scope.Notifications.Publish(new UserGroupSavedNotification(userGroup, evtMsgs).WithStateFrom(savingNotification)); - scope.Notifications.Publish(new UserGroupWithUsersSavedNotification(userGroupWithUsers, evtMsgs).WithStateFrom(savingUserGroupWithUsersNotification)); + 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; } - } - - /// - /// Deletes a UserGroup - /// - /// UserGroup to delete - public void DeleteUserGroup(IUserGroup userGroup) - { - var evtMsgs = EventMessagesFactory.Get(); - using (var scope = ScopeProvider.CreateCoreScope()) + // this is an additional notification for special auditing + var savingUserGroupWithUsersNotification = + new UserGroupWithUsersSavingNotification(userGroupWithUsers, evtMsgs); + if (scope.Notifications.PublishCancelable(savingUserGroupWithUsersNotification)) { - var deletingNotification = new UserGroupDeletingNotification(userGroup, evtMsgs); - if (scope.Notifications.PublishCancelable(deletingNotification)) - { - scope.Complete(); - return; - } + scope.Complete(); + return; + } - _userGroupRepository.Delete(userGroup); + _userGroupRepository.AddOrUpdateGroupWithUsers(userGroup, userIds); - scope.Notifications.Publish(new UserGroupDeletedNotification(userGroup, evtMsgs).WithStateFrom(deletingNotification)); + scope.Notifications.Publish( + new UserGroupSavedNotification(userGroup, evtMsgs).WithStateFrom(savingNotification)); + scope.Notifications.Publish( + new UserGroupWithUsersSavedNotification(userGroupWithUsers, evtMsgs).WithStateFrom( + savingUserGroupWithUsersNotification)); - scope.Complete(); - } + 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) + /// + /// Deletes a UserGroup + /// + /// UserGroup to delete + public void DeleteUserGroup(IUserGroup userGroup) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (var scope = ScopeProvider.CreateCoreScope()) + var deletingNotification = new UserGroupDeletingNotification(userGroup, evtMsgs); + if (scope.Notifications.PublishCancelable(deletingNotification)) { - var assignedGroups = _userGroupRepository.GetGroupsAssignedToSection(sectionAlias); - foreach (var 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(); + return; } + + _userGroupRepository.Delete(userGroup); + + scope.Notifications.Publish( + new UserGroupDeletedNotification(userGroup, evtMsgs).WithStateFrom(deletingNotification)); + + 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) + /// + /// 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()) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + IEnumerable assignedGroups = _userGroupRepository.GetGroupsAssignedToSection(sectionAlias); + foreach (IUserGroup group in assignedGroups) { - return _userGroupRepository.GetPermissions(user?.Groups.ToArray(), true, nodeIds); + // 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 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) + /// + /// 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)) { - if (groups == null) throw new ArgumentNullException(nameof(groups)); + return _userGroupRepository.GetPermissions(user?.Groups.ToArray(), true, nodeIds); + } + } - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userGroupRepository.GetPermissions(groups, fallbackToDefaultPermissions, 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)); } - /// - /// 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) + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - 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); - } + 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) + } + + /// + /// 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) { - var nodeIds = path?.GetIdsFromPathReversed(); + throw new ArgumentNullException(nameof(groups)); + } - if (nodeIds is null || nodeIds.Length == 0 || user is null) - return EntityPermissionSet.Empty(); + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userGroupRepository.GetPermissions(groups, fallbackToDefaultPermissions, nodeIds); + } + } - //collect all permissions structures for all nodes for all groups belonging to the user - var groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, fallbackToDefaultPermissions: true).ToArray(); + /// + /// 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(); - return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); + if (nodeIds is null || nodeIds.Length == 0 || user is null) + { + return EntityPermissionSet.Empty(); } - /// - /// 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(); + // collect all permissions structures for all nodes for all groups belonging to the user + EntityPermission[] groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, true).ToArray(); - if (nodeIds.Length == 0) - return EntityPermissionSet.Empty(); + return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); + } - //collect all permissions structures for all nodes for all groups - var groupPermissions = GetPermissionsForPath(groups.Select(x => x.ToReadOnlyGroup()).ToArray(), nodeIds, fallbackToDefaultPermissions: true).ToArray(); + /// + /// 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(); - return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); + if (nodeIds.Length == 0) + { + return EntityPermissionSet.Empty(); } - private EntityPermissionCollection GetPermissionsForPath(IReadOnlyUserGroup[] groups, int[] pathIds, bool fallbackToDefaultPermissions = false) - { - if (pathIds.Length == 0) - return new EntityPermissionCollection(Enumerable.Empty()); + // collect all permissions structures for all nodes for all groups + EntityPermission[] groupPermissions = + GetPermissionsForPath(groups.Select(x => x.ToReadOnlyGroup()).ToArray(), nodeIds, true).ToArray(); - //get permissions for all nodes in the path by group - var permissions = GetPermissions(groups, fallbackToDefaultPermissions, pathIds) - .GroupBy(x => x.UserGroupId); + return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); + } - 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(); } - /// - /// 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]; - //The actual entity id being looked at (deepest part of the path) - var entityId = pathIds[0]; + var resultPermissions = new EntityPermissionCollection(); - 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())); - //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 through each group - foreach (var byGroup in permissionsByGroup) + // iterate deepest to shallowest + foreach (var pathId in pathIds) { - var added = false; + if (byGroup.Value.TryGetValue(pathId, out EntityPermission[]? permissionsForNodeAndGroup) == false) + { + continue; + } - //iterate deepest to shallowest - foreach (var pathId in pathIds) + // 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) { - 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) { - 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) + // 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 (added == false && byGroup.Value.Count > 0) + // if the permission has been added for this group and this branch then we can exit this loop + if (added) { - //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]); + break; } - } - var permissionSet = new EntityPermissionSet(entityId, resultPermissions); - return permissionSet; + 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]); + } } - /// - /// 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) + var permissionSet = new EntityPermissionSet(entityId, resultPermissions); + return permissionSet; + } + + private EntityPermissionCollection GetPermissionsForPath(IReadOnlyUserGroup[] groups, int[] pathIds, bool fallbackToDefaultPermissions = false) + { + if (pathIds.Length == 0) { - //get permissions for all nodes in the path - var permissionsByEntityId = pathPermissions.ToDictionary(x => x.EntityId, x => x); + return new EntityPermissionCollection(Enumerable.Empty()); + } - //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; - } - } + // get permissions for all nodes in the path by group + IEnumerable> permissions = + GetPermissions(groups, fallbackToDefaultPermissions, pathIds) + .GroupBy(x => x.UserGroupId); - //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 new EntityPermissionCollection( + permissions.Select(x => GetPermissionsForPathForGroup(x, pathIds, fallbackToDefaultPermissions)) + .Where(x => x is not null)!); + } - return permissionsByEntityId[pathIds[0]]; - } + /// + /// 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); - /// - /// 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) + // then the permissions assigned to the path will be the 'deepest' node found that has permissions + foreach (var id in pathIds) { - if (permissions.Any(x => x.EntityId == nodeId)) + if (permissionsByEntityId.TryGetValue(id, out EntityPermission? permission)) { - 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)) + // 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) { - AddAdditionalPermissions(assignedPermissionsArray, permission.AssignedPermissions); + return permission; } - - assignedPermissions = string.Join("", assignedPermissionsArray); - return true; } - - assignedPermissions = string.Empty; - return false; } - private static void AddAdditionalPermissions(List assignedPermissions, string[] additionalPermissions) + // 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) { - var permissionsToAdd = additionalPermissions - .Where(x => assignedPermissions.Contains(x) == false); - assignedPermissions.AddRange(permissionsToAdd); + return null; } - #endregion + 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 86e823f8bc69..f17a26661681 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) 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(); - } - - /// - /// 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) + 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) { - return service.GetPermissions(new[] {group}, fallbackToDefaultPermissions, nodeIds); + throw new InvalidOperationException("The path: " + path + + " could not be parsed into an array of integers or the path was empty"); } - /// - /// 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); - } + return userService.GetPermissions(user, ids[^1]).FirstOrDefault(); + } - /// - /// Remove all permissions for this user group for all nodes - /// - /// - /// - public static void RemoveUserGroupPermissions(this IUserService userService, int groupId) - { - userService.ReplaceUserGroupPermissions(groupId, null); - } + /// + /// 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); - public static IEnumerable GetProfilesById(this IUserService userService, params int[] ids) - { - var fullUsers = userService.GetUsersById(ids); + /// + /// 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); - return fullUsers.Select(user => - { - var asProfile = user as IProfile; - return asProfile ?? new UserProfile(user.Id, user.Name); - }); + /// + /// 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); - public static IUser? GetByKey(this IUserService userService, Guid key) + return fullUsers.Select(user => { - int id = BitConverter.ToInt32(key.ToByteArray(), 0); - return userService.GetUserById(id); - } + var asProfile = user as IProfile; + return asProfile ?? new UserProfile(user.Id, user.Name); + }); + } + + 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 07f53c208072..9f91ee15ff34 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 - { - private T? _value; + public bool HasValue { get; private set; } - /// - /// Assigns a value to this instance. - /// - /// The value. - public void Set(T? value) + /// + /// 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 (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); + return _value; } + } - /// - /// Clears the value. - /// - public void Clear() + /// + /// Assigns a value to this instance. + /// + /// The value. + public void Set(T? value) + { + if (value is not null) { - HasValue = false; - _value = default (T); + HasValue = true; } - /// - /// Gets a value indicating whether a value has been assigned to this instance. - /// - public bool HasValue { get; private set; } + _value = value; + } - /// - /// 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 + /// + /// 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) { - get - { - if (HasValue == false) - throw new InvalidOperationException("The HasValue property is false."); - return _value; - } + Set(other.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); - } + /// + /// Clears the value. + /// + public void Clear() + { + HasValue = false; + _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) - { - return HasValue ? _value : defaultValue; - } + /// + /// 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; - /// - public override string? ToString() - { - return HasValue ? _value?.ToString() : "void"; - } - } + /// + /// 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 3f4bd1ce7cae..3b3bc1b0c017 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 object _locko = new object(); - private readonly List> _callbacks = new List>(); - private bool _isStopping; - private bool _disposedValue; + private readonly List> _callbacks = new(); + private readonly object _locko = new(); + private bool _disposedValue; + private bool _isStopping; + + /// + public bool IsMainDom { get; private set; } = true; - /// - public bool IsMainDom { get; private set; } = true; + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); + GC.SuppressFinalize(this); + } - // 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; } - } - public void Stop() - { - lock (_locko) + install?.Invoke(); + if (release != null) { - if (_isStopping) return; - if (IsMainDom == false) return; // probably not needed - _isStopping = true; + _callbacks.Add(new KeyValuePair(weight, release)); } - try + return true; + } + } + + public void Stop() + { + lock (_locko) + { + if (_isStopping) { - foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) - { - callback(); // no timeout on callbacks - } + return; } - finally + + if (IsMainDom == false) { - // in any case... - IsMainDom = false; + return; // probably not needed } + + _isStopping = true; } - protected virtual void Dispose(bool disposing) + try { - if (!_disposedValue) + foreach (Action callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) { - if (disposing) - { - Stop(); - } - _disposedValue = true; + callback(); // no timeout on callbacks } } + finally + { + // 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 f0d01d40735e..eac0a3f51b4f 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 3435c81780f9..2b1229be77c5 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 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; - } + /// 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)); - } + /// + /// 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 + /// + /// The string part of the identifier. + /// + public string Id { get; } - // reserved = : / ? # [ ] @ ! $ & ' ( ) * + , ; = - // unreserved = alpha digit - . _ ~ + /// + public override bool IsRoot => Id == string.Empty; - // we want to preserve the / and the unreserved - // so... - return string.Join("/", s.Split(Constants.CharArrays.ForwardSlash).Select(Uri.EscapeDataString)); - } + public StringUdi EnsureClosed() + { + EnsureNotRoot(); + return this; + } - /// - public override bool IsRoot - { - get { return Id == string.Empty; } - } + private static string EscapeUriString(string s) => - public StringUdi EnsureClosed() - { - EnsureNotRoot(); - return this; - } - } + // 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 771e834d35db..75ad000505c5 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 + + /// + /// No value. + /// + None = 0x00, + + // casing values + /// - /// Specifies the type of a clean string. + /// 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 - - // no value - - /// - /// 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. - /// - /// 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, - - - // 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 - } + UmbracoCase = 0x20, + + // 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, } diff --git a/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs b/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs index a95a3edfc29c..e2eb3df7a446 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; } - } - - return rules; - } + rules.Add(new StylesheetRule + { + Name = match.Groups["Name"].Value, + Selector = match.Groups["Selector"].Value, - public static string? ReplaceRule(string? input, string oldRuleName, StylesheetRule? rule) - { - 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() : ""); + // 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 contents; } - public static string AppendRule(string? input, 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; - contents += Environment.NewLine + Environment.NewLine + rule; - 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); } + + 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 06a888c81221..4b726f34ef05 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 Name { get; set; } = null!; - public string Selector { get; set; } = null!; + public string Selector { get; set; } = null!; - public string Styles { get; set; } = null!; + public string Styles { get; set; } = null!; + + public override string ToString() + { + 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 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 d4d781c9bcda..ea93a099f840 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 @@ public virtual string CleanStringForSafeFileName(string text) 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 @@ public virtual string CleanStringForSafeFileName(string text, string culture) 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 @@ public virtual string CleanStringForSafeFileName(string text, string culture) /// 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 @@ public string CleanString(string text, CleanStringType stringType) /// 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 @@ public string CleanString(string text, CleanStringType stringType, char separato 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 @@ protected virtual string CleanString(string text, CleanStringType stringType, st 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 @@ protected virtual string CleanString(string text, CleanStringType stringType, st // apply post-filter if (config.PostFilter != null) + { text = config.PostFilter(text); + } return text; } @@ -323,7 +338,7 @@ internal string CleanCodeString(string text, CleanStringType caseType, char sepa 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 @@ internal string CleanCodeString(string text, CleanStringType caseType, char sepa { ipos = i; if (opos > 0 && separator != char.MinValue) + { output[opos++] = separator; + } + state = isUpper ? StateUp : StateWord; } + break; // within a term / word @@ -379,8 +398,11 @@ internal string CleanCodeString(string text, CleanStringType caseType, char sepa ipos = i; state = isTerm ? StateUp : StateBreak; if (state != StateBreak && separator != char.MinValue) + { output[opos++] = separator; + } } + break; // within a term / acronym @@ -391,14 +413,19 @@ internal string CleanCodeString(string text, CleanStringType caseType, char sepa { // 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 @@ internal string CleanCodeString(string text, CleanStringType caseType, char sepa // keep moving forward as a word state = StateWord; } + break; // within a term / uppercase = could be a word or an acronym @@ -455,18 +483,19 @@ internal string CleanCodeString(string text, CleanStringType caseType, char sepa } // 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 @@ public virtual string SplitPascalCasing(string text, char separator) { // 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 @@ public virtual string SplitPascalCasing(string text, char separator) if (upos == 0) { if (opos > 0) + { output[opos++] = separator; + } + upos = i + 1; } } @@ -612,15 +646,24 @@ public virtual string SplitPascalCasing(string text, char separator) 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 4f2d20215524..ec7ed9d0023c 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 +namespace Umbraco.Cms.Core.Strings; + +public class DefaultShortStringHelperConfig { - public class DefaultShortStringHelperConfig - { - private readonly Dictionary> _configs = new Dictionary>(); + private readonly Dictionary> _configs = new(); + + public string DefaultCulture { get; set; } = string.Empty; // invariant + + public Dictionary? UrlReplaceCharacters { get; set; } - public DefaultShortStringHelperConfig Clone() + public DefaultShortStringHelperConfig Clone() + { + var config = new DefaultShortStringHelperConfig { - var config = new DefaultShortStringHelperConfig - { - DefaultCulture = DefaultCulture, - UrlReplaceCharacters = UrlReplaceCharacters - }; + DefaultCulture = DefaultCulture, + UrlReplaceCharacters = UrlReplaceCharacters, + }; - foreach (var kvp1 in _configs) + foreach (KeyValuePair> kvp1 in _configs) + { + Dictionary c = config._configs[kvp1.Key] = + new Dictionary(); + foreach (KeyValuePair kvp2 in _configs[kvp1.Key]) { - var c = config._configs[kvp1.Key] = new Dictionary(); - foreach (var kvp2 in _configs[kvp1.Key]) - c[kvp2.Key] = kvp2.Value.Clone(); + c[kvp2.Key] = kvp2.Value.Clone(); } - - return config; } - public string DefaultCulture { get; set; } = ""; // invariant + return config; + } + + public DefaultShortStringHelperConfig WithConfig(Config config) => + WithConfig(DefaultCulture, CleanStringType.RoleMask, config); - public Dictionary? UrlReplaceCharacters { get; set; } + public DefaultShortStringHelperConfig WithConfig(CleanStringType stringRole, Config config) => + WithConfig(DefaultCulture, stringRole, config); - public DefaultShortStringHelperConfig WithConfig(Config config) + public DefaultShortStringHelperConfig WithConfig(string? culture, CleanStringType stringRole, Config config) + { + if (config == null) { - return WithConfig(DefaultCulture, CleanStringType.RoleMask, config); + throw new ArgumentNullException(nameof(config)); } - public DefaultShortStringHelperConfig WithConfig(CleanStringType stringRole, Config config) + culture = culture ?? string.Empty; + + if (_configs.ContainsKey(culture) == false) { - return WithConfig(DefaultCulture, stringRole, config); + _configs[culture] = new Dictionary(); } - public DefaultShortStringHelperConfig WithConfig(string culture, CleanStringType stringRole, Config config) - { - if (config == null) throw new ArgumentNullException(nameof(config)); + _configs[culture][stringRole] = config; + return this; + } - culture = culture ?? ""; + /// + /// Sets the default configuration. + /// + /// The short string helper. + public DefaultShortStringHelperConfig WithDefault(RequestHandlerSettings requestHandlerSettings) + { + IEnumerable charCollection = requestHandlerSettings.GetCharReplacements(); - if (_configs.ContainsKey(culture) == false) - _configs[culture] = new Dictionary(); - _configs[culture][stringRole] = config; - return this; - } + UrlReplaceCharacters = charCollection + .Where(x => string.IsNullOrEmpty(x.Char) == false) + .ToDictionary(x => x.Char, x => x.Replacement); - /// - /// Sets the default configuration. - /// - /// The short string helper. - public DefaultShortStringHelperConfig WithDefault(RequestHandlerSettings requestHandlerSettings) + CleanStringType urlSegmentConvertTo = CleanStringType.Utf8; + if (requestHandlerSettings.ShouldConvertUrlsToAscii) { - IEnumerable charCollection = requestHandlerSettings.GetCharReplacements(); + urlSegmentConvertTo = CleanStringType.Ascii; + } - UrlReplaceCharacters = charCollection - .Where(x => string.IsNullOrEmpty(x.Char) == false) - .ToDictionary(x => x.Char, x => x.Replacement); + if (requestHandlerSettings.ShouldTryConvertUrlsToAscii) + { + urlSegmentConvertTo = CleanStringType.TryAscii; + } - var 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, + }); + } - 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; - // 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) + Dictionary config; + if (_configs.ContainsKey(culture)) { - culture = culture ?? ""; - stringType = stringType & CleanStringType.RoleMask; + config = _configs[culture]; - Dictionary config; - if (_configs.ContainsKey(culture)) + // have we got a config for _that_ role? + if (config.ContainsKey(stringType)) { - 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]; + return config[stringType]; } - else if (_configs.ContainsKey(DefaultCulture)) + + // have we got a generic config for _all_ roles? + if (config.ContainsKey(CleanStringType.RoleMask)) { - 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[CleanStringType.RoleMask]; } - - return Config.NotConfigured; } - - public sealed class Config + else if (_configs.ContainsKey(DefaultCulture)) { - public Config() + config = _configs[DefaultCulture]; + + // have we got a config for _that_ role? + if (config.ContainsKey(stringType)) { - 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; + return config[stringType]; } - public Config Clone() + // have we got a generic config for _all_ roles? + if (config.ContainsKey(CleanStringType.RoleMask)) { - return new Config - { - PreFilter = PreFilter, - PostFilter = PostFilter, - IsTerm = IsTerm, - StringType = StringType, - BreakTermsOnUpper = BreakTermsOnUpper, - CutAcronymOnNonUpper = CutAcronymOnNonUpper, - GreedyAcronyms = GreedyAcronyms, - Separator = Separator - }; + return config[CleanStringType.RoleMask]; } + } - public Func? PreFilter { get; set; } - public Func? PostFilter { get; set; } - public Func IsTerm { get; set; } + return Config.NotConfigured; + } - public CleanStringType StringType { get; set; } + /// + /// 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); - // 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; } + public static string CutMaxLength(string text, int length) => + text.Length <= length ? text : text.Substring(0, length); - // 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; } + public sealed class Config + { + internal static readonly Config NotConfigured = new(); - // indicates whether acronyms parsing is greedy ie whether "FOObar" is - // "FOO" + "bar" (greedy) or "FO" + "Obar" (non-greedy) - public bool GreedyAcronyms { get; set; } + 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; + } - // the separator char - // but then how can we tell we don't want any? - public char Separator { get; set; } + public Func? PreFilter { 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; + public Func? PostFilter { get; set; } - st = st & ~mask; // clear what we have - st = st | a; // set the new value - } - return st; - } + public Func IsTerm { get; set; } - internal static readonly Config NotConfigured = new Config(); - } + public CleanStringType StringType { get; set; } - /// - /// 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); - } + // 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; } - public static string CutMaxLength(string text, int length) + // 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, + }; + + // extends the config + public CleanStringType StringTypeExtend(CleanStringType stringType) { - return text.Length <= length ? text : text.Substring(0, length); + CleanStringType st = StringType; + foreach (CleanStringType mask in new[] { CleanStringType.CaseMask, CleanStringType.CodeMask }) + { + CleanStringType a = stringType & mask; + if (a == 0) + { + continue; + } + + st = st & ~mask; // clear what we have + st = st | a; // set the new value + } + + return st; } } } diff --git a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs index 22b8a40c0e2e..36c0d6e85ef3 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 - { - private readonly IShortStringHelper _shortStringHelper; + /// The content. + /// The culture. + /// The URL segment. + public string? GetUrlSegment(IContentBase content, string? culture = null) => + GetUrlSegmentSource(content, culture)?.ToUrlSegment(_shortStringHelper, culture); - public DefaultUrlSegmentProvider(IShortStringHelper shortStringHelper) + private static string? GetUrlSegmentSource(IContentBase content, string? culture) + { + 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 8d7ef9feaa37..e8a3fdf84c2f 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 - { - /// Data on one input file being compared. - /// - internal class DiffData - { + /// 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 - /// Number of elements (lines). - internal int Length; - - /// Buffer of numbers that will be compared. - internal int[] Data; + /// + /// 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)); - /// - /// 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; + /// + /// 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) + { + // prepare the input-text and convert to comparable numbers. + var h = new Hashtable(textA.Length + textB.Length); - /// - /// Initialize the Diff-Data buffer. - /// - /// reference to the buffer - internal DiffData(int[] initData) - { - Data = initData; - Length = initData.Length; - Modified = new bool[Length + 2]; - } // DiffData + // The A-Version of the data (original data) to be compared. + var dataA = new DiffData(DiffCodes(textA, h, trimSpace, ignoreSpace, ignoreCase)); - } // class DiffData + // The B-Version of the data (modified data) to be compared. + var dataB = new DiffData(DiffCodes(textB, h, trimSpace, ignoreSpace, ignoreCase)); - /// 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; + h = null; // free up Hashtable memory (maybe) - /// Number of changes in Data A. - public int DeletedA; - /// Number of changes in Data B. - public int InsertedB; - } // Item + var max = dataA.Length + dataB.Length + 1; - /// - /// Shortest Middle Snake Return Data - /// - private struct Smsrd - { - internal int X, Y; - // internal int u, v; // 2002.09.20: no need for 2 points - } - - /// - /// 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 - - /// - /// 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) - { - return DiffInt(DiffCharCodes(textA, false), DiffCharCodes(textB, false)); - } + // 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]; - /// - /// 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) - { - // prepare the input-text and convert to comparable numbers. - var h = new Hashtable(textA.Length + textB.Length); + Lcs(dataA, 0, dataA.Length, dataB, 0, dataB.Length, downVector, upVector); - // The A-Version of the data (original data) to be compared. - var dataA = new DiffData(DiffCodes(textA, h, trimSpace, ignoreSpace, ignoreCase)); + Optimize(dataA); + Optimize(dataB); + return CreateDiffs(dataA, dataB); + } // DiffText - // The B-Version of the data (modified data) to be compared. - var dataB = new DiffData(DiffCodes(textB, h, trimSpace, ignoreSpace, ignoreCase)); + /// + /// 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); - h = null; // free up Hashtable memory (maybe) + // 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]; + var max = dataA.Length + dataB.Length + 1; - Lcs(dataA, 0, dataA.Length, dataB, 0, dataB.Length, downVector, upVector); + // vector for the (0,0) to (x,y) search + var downVector = new int[(2 * max) + 2]; - Optimize(dataA); - Optimize(dataB); - return CreateDiffs(dataA, dataB); - } // DiffText + // 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) + /// + /// Diffs the char codes. + /// + /// A text. + /// if set to true [ignore case]. + /// + private static int[] DiffCharCodes(string aText, bool ignoreCase) + { + if (ignoreCase) { - if (ignoreCase) - aText = aText.ToUpperInvariant(); + aText = aText.ToUpperInvariant(); + } - var codes = new int[aText.Length]; + var codes = new int[aText.Length]; - for (int n = 0; n < aText.Length; n++) - codes[n] = (int)aText[n]; + for (var n = 0; n < aText.Length; n++) + { + codes[n] = aText[n]; + } - return (codes); - } // DiffCharCodes + 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) + /// + /// 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) { - 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 - + startPos++; + } - /// - /// 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); + var endPos = startPos; + while (endPos < data.Length && data.Modified[endPos]) + { + endPos++; + } - // The B-Version of the data (modified data) to be compared. - var dataB = new DiffData(arrayB); + if (endPos < data.Length && data.Data[startPos] == data.Data[endPos]) + { + data.Modified[startPos] = false; + data.Modified[endPos] = true; + } + else + { + startPos = endPos; + } // if + } // while + } // Optimize - 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]; + /// + /// 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; - Lcs(dataA, 0, dataA.Length, dataB, 0, dataB.Length, downVector, upVector); - return CreateDiffs(dataA, dataB); - } // Diff + // strip off all cr, only use lf as text line separator. + aText = aText.Replace("\r", string.Empty); + var lines = aText.Split(Constants.CharArrays.LineFeed); + var codes = new int[lines.Length]; - /// - /// 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) + for (var i = 0; i < lines.Length; ++i) { - // 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) + var s = lines[i]; + if (trimSpace) { - string s = lines[i]; - if (trimSpace) - s = s.Trim(); + s = s.Trim(); + } - if (ignoreSpace) - { - s = Regex.Replace(s, "\\s+", " "); // TODO: optimization: faster blank removal. - } + if (ignoreSpace) + { + s = Regex.Replace(s, "\\s+", " "); // TODO: optimization: faster blank removal. + } - if (ignoreCase) - s = s.ToLower(); + 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 + 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) - { - int max = dataA.Length + dataB.Length + 1; + /// + /// 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; - int downK = lowerA - lowerB; // the k-line to start the forward search - int upK = upperA - upperB; // the k-line to start the reverse search + var downK = lowerA - lowerB; // the k-line to start the forward search + var upK = upperA - upperB; // the k-line to start the reverse search - int delta = (upperA - lowerA) - (upperB - lowerB); - bool oddDelta = (delta & 1) != 0; + 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 - int downOffset = max - downK; - int upOffset = max - upK; + // 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; - int maxD = ((upperA - lowerA + upperB - lowerB) / 2) + 1; + 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)); + // 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; + // init vectors + downVector[downOffset + downK + 1] = lowerA; + upVector[upOffset + upK - 1] = upperA; - for (int d = 0; d <= maxD; d++) + 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()); - // Extend the forward path. - Smsrd ret; - for (int k = downK - d; k <= downK + d; k += 2) + // find the only or better starting point + int x, y; + if (k == downK - d) { - // 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 } - 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; + y = x - k; - // overlap ? - if (oddDelta && (upK - d < k) && (k < upK + d)) + // 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]) { - 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 + 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 k + // Extend the reverse path. + for (var k = upK - d; k <= upK + d; k += 2) + { + // Debug.Write(0, "SMS", "extend reverse path " + k.ToString()); - // Extend the reverse path. - for (int k = upK - d; k <= upK + d; k += 2) + // find the only or better starting point + int x, y; + if (k == upK + d) { - // 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 } - 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; + } // if - while ((x > lowerA) && (y > lowerB) && (dataA.Data[x - 1] == dataB.Data[y - 1])) - { - x--; y--; // diagonal - } - upVector[upOffset + k] = x; + 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)) + // overlap ? + if (!oddDelta && downK - d <= k && k <= downK + d) + { + if (upVector[upOffset + k] <= downVector[downOffset + k]) { - 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 + ret.X = downVector[downOffset + k]; + ret.Y = downVector[downOffset + k] - k; - } // for 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 - } // for D + throw new ApplicationException("the algorithm should never come here."); + } // SMS - 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++; + } - /// - /// 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) + // Fast walk through equal lines at the end + while (lowerA < upperA && lowerB < upperB && dataA.Data[upperA - 1] == dataB.Data[upperB - 1]) { - // Debug.Write(2, "LCS", String.Format("Analyze the box: A[{0}-{1}] to B[{2}-{3}]", LowerA, UpperA, LowerB, UpperB)); + --upperA; + --upperB; + } - // Fast walk through equal lines at the start - while (lowerA < upperA && lowerB < upperB && dataA.Data[lowerA] == dataB.Data[lowerB]) + if (lowerA == upperA) + { + // mark as inserted lines. + while (lowerB < upperB) { - lowerA++; lowerB++; + dataB.Modified[lowerB++] = true; } - - // Fast walk through equal lines at the end - while (lowerA < upperA && lowerB < upperB && dataA.Data[upperA - 1] == dataB.Data[upperB - 1]) + } + else if (lowerB == upperB) + { + // mark as deleted lines. + while (lowerA < upperA) { - --upperA; --upperB; + 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); - if (lowerA == upperA) - { - // mark as inserted lines. - while (lowerB < upperB) - dataB.Modified[lowerB++] = true; + // Debug.Write(2, "MiddleSnakeData", String.Format("{0},{1}", smsrd.x, smsrd.y)); - } - else if (lowerB == upperB) - { - // mark as deleted lines. - while (lowerA < upperA) - dataA.Modified[lowerA++] = true; + // 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 { - // 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)); + // maybe deleted and/or inserted lines + var startA = lineA; + var startB = lineB; - // 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() + while (lineA < dataA.Length && (lineB >= dataB.Length || dataA.Modified[lineA])) + // while (LineA < DataA.Length && DataA.modified[LineA]) + { + lineA++; + } - /// 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) - { - ArrayList a = new ArrayList(); - Item aItem; - Item[] result; + while (lineB < dataB.Length && (lineA >= dataA.Length || dataB.Modified[lineB])) - int lineA = 0; - int lineB = 0; - while (lineA < dataA.Length || lineB < dataB.Length) - { - if ((lineA < dataA.Length) && (!dataA.Modified[lineA]) - && (lineB < dataB.Length) && (!dataB.Modified[lineB])) + // while (LineB < DataB.Length && DataB.modified[LineB]) { - // equal lines - lineA++; lineB++; - } - else + + if (startA < lineA || startB < lineB) { - // maybe deleted and/or inserted lines - int startA = lineA; - int 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 - while (lineA < dataA.Length && (lineB >= dataB.Length || dataA.Modified[lineA])) - // while (LineA < DataA.Length && DataA.modified[LineA]) - lineA++; + result = new Item[a.Count]; + a.CopyTo(result); - while (lineB < dataB.Length && (lineA >= dataA.Length || dataB.Modified[lineB])) - // while (LineB < DataB.Length && DataB.modified[LineB]) - lineB++; + return result; + } - if ((startA < lineA) || (startB < lineB)) - { - // 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); - } // if - } // if - } // while + /// details of one difference. + public struct Item + { + /// Start Line number in Data A. + public int StartA; - result = new Item[a.Count]; - a.CopyTo(result); + /// Start Line number in Data B. + public int StartB; - return (result); - } + /// Number of changes in Data A. + public int DeletedA; + + /// Number of changes in Data B. + public int InsertedB; + } // Item - } // class Diff + /// + /// 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 16941cef484b..4477d5436cb7 100644 --- a/src/Umbraco.Core/Strings/HtmlEncodedString.cs +++ b/src/Umbraco.Core/Strings/HtmlEncodedString.cs @@ -1,33 +1,21 @@ -namespace Umbraco.Cms.Core.Strings -{ - /// - /// Represents an HTML-encoded string that should not be encoded again. - /// - public class HtmlEncodedString : IHtmlEncodedString - { - - private string _htmlString; +namespace Umbraco.Cms.Core.Strings; - /// Initializes a new instance of the class. - /// An HTML-encoded string that should not be encoded again. - public HtmlEncodedString(string value) - { - this._htmlString = value; - } +/// +/// Represents an HTML-encoded string that should not be encoded again. +/// +public class HtmlEncodedString : IHtmlEncodedString +{ + private readonly string _htmlString; - /// Returns an HTML-encoded string. - /// An HTML-encoded string. - public string ToHtmlString() - { - return this._htmlString; - } + /// Initializes a new instance of the class. + /// An HTML-encoded string that should not be encoded again. + public HtmlEncodedString(string value) => _htmlString = value; - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - { - return this._htmlString; - } + /// Returns an HTML-encoded string. + /// An HTML-encoded string. + public string ToHtmlString() => _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 b7c0c27d2da3..bf94f834ad66 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 a436758d9acb..a5c20f1a09fb 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 74d147173f08..c7050050e168 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 bc88fa8bcaae..cab7127a6ef9 100644 --- a/src/Umbraco.Core/Strings/PathUtility.cs +++ b/src/Umbraco.Core/Strings/PathUtility.cs @@ -1,22 +1,29 @@ -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +public static class PathUtility { - public static class PathUtility + /// + /// Ensures that a path has `~/` as prefix + /// + /// + /// + public static string EnsurePathIsApplicationRootPrefixed(string path) { - - /// - /// 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 551efc475a56..39b826dae98d 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 60504734f653..f9aa13b335da 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 3f492a7b87ad..4221273150ac 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(); + var input = text.ToCharArray(); - // this is faster although it uses more memory - // but... we should be filtering short strings only... + // 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 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(); + } - //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; - /// - /// 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 = '?') + 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); - 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; + // ignore high surrogate + if (char.IsSurrogate(input[ipos])) + { + ipos++; // and skip low surrogate + output[opos++] = fail; + } + else + { + ToAscii(input, ipos, output, ref opos, fail); + } } - /// - /// 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; + return opos; + } - 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); + // 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); + // } + // } + // } - return 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]; - //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 = '?') + if (char.IsControl(c)) { - var c = 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. - 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 + } - // 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 + // 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) { - 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': // G [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': // V [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'; + 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 '€': + 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'; - output[opos++] = 'R'; break; - case '©': + 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; - */ - 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; + 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; - // strict ASCII - output[opos++] = fail; + case '\u01C4': + // Ç„ [LATIN CAPITAL LETTER DZ WITH CARON] + case '\u01F1': // DZ [LATIN CAPITAL LETTER DZ] + output[opos++] = 'D'; + output[opos++] = 'Z'; + break; - 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; - //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; - //} - } + 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': // G [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': // V [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 340de80c96f6..09c904b7bcff 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; - - /// - /// Initializes a new instance of the class. - /// - /// The registration service. - /// Some options. - public ElectedServerRoleAccessor(IServerRegistrationService registrationService) => _registrationService = registrationService ?? throw new ArgumentNullException(nameof(registrationService)); + /// 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 4de7490d8fe3..cc9da01db052 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 e58cfe9bc019..49cd397e2d79 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 1ebd59b26dcf..aed70b0f50fc 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 0c616a4e6873..1d7d085f90de 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 51644286322d..282aebeb54b3 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 0dcfa471db16..4040edd8f723 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 b8609410ab6c..2a80dbf95f85 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; - } + 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) - : this() + 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) { - RefresherId = refresher.RefresherUniqueId; - RefreshType = refreshType; + JsonPayload = json; } - - 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) + else { - JsonIdCount = idCount; - - if (refreshType == RefreshMethodType.RefreshByJson) - { - JsonPayload = json; - } - else - { - JsonIds = json; - } + JsonIds = json; } + } - public static IEnumerable GetInstructions( - ICacheRefresher refresher, - IJsonSerializer jsonSerializer, - MessageType messageType, - IEnumerable? ids, - Type? idType, - string? 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) { - 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) + 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[] { - 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(); + 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"); + } + } - 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; } - /// - /// 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 (ReferenceEquals(this, other)) { - if (other is null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - if (other.GetType() != GetType()) - { - return false; - } - - return Equals((RefreshInstruction) other); + return true; } - public override int GetHashCode() + if (other.GetType() != GetType()) { - 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; - } + return false; } - public static bool operator ==(RefreshInstruction left, RefreshInstruction right) => Equals(left, right); + 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 static bool operator !=(RefreshInstruction left, RefreshInstruction right) => Equals(left, right) == false; + 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 bf72423c1f7f..f249a4701e15 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 9bfd4469b38c..15f546fc3594 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 2f4e85c5b1ff..f03f27d9e7bc 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 670930de3171..6233ace01ae2 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 d39d6ecbcef9..0e47096c2e0c 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; - -namespace Umbraco.Cms.Core +using System.Runtime.ConstrainedExecution; + +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 SemaphoreSlim? _semaphore; - private readonly Semaphore? _semaphore2; - private readonly IDisposable? _releaser; - private readonly Task? _releaserTask; + private readonly IDisposable? _releaser; + private readonly Task? _releaserTask; + private readonly SemaphoreSlim? _semaphore; + private readonly Semaphore? _semaphore2; - public SystemLock() - : this(null) - { } + 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 - - if (string.IsNullOrWhiteSpace(name)) - { - // 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 - } + // 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); } - - private IDisposable? CreateReleaser() + else { - // for anonymous semaphore, use the unique releaser, else create a new one - return _semaphore != null - ? _releaser // (IDisposable)new SemaphoreSlimReleaser(_semaphore) - : new NamedSemaphoreReleaser(_semaphore2); + // 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 } + } - public IDisposable? Lock() + public IDisposable? Lock() + { + if (_semaphore != null) { - if (_semaphore != null) - _semaphore.Wait(); - else - _semaphore2?.WaitOne(); - return _releaser ?? CreateReleaser(); // anonymous vs named + _semaphore.Wait(); } - - public IDisposable? Lock(int millisecondsTimeout) + else { - 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 + _semaphore2?.WaitOne(); } - // note - before making those classes some structs, read - // about "impure methods" and mutating readonly structs... - - private class NamedSemaphoreReleaser : CriticalFinalizerObject, IDisposable - { - private readonly Semaphore? _semaphore; + return _releaser ?? CreateReleaser(); // anonymous vs named + } - internal NamedSemaphoreReleaser(Semaphore? semaphore) - { - _semaphore = semaphore; - } + private IDisposable? CreateReleaser() => - #region IDisposable Support + // for anonymous semaphore, use the unique releaser, else create a new one + _semaphore != null + ? _releaser // (IDisposable)new SemaphoreSlimReleaser(_semaphore) + : new NamedSemaphoreReleaser(_semaphore2); - // This code added to correctly implement the disposable pattern. + 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."); + } - private bool disposedValue = false; // To detect redundant calls + return _releaser ?? CreateReleaser(); // anonymous vs named + } - public void Dispose() + // 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 { - Dispose(true); - GC.SuppressFinalize(this); // finalize will not run + Dispose(false); } - - private void Dispose(bool disposing) + catch { - if (!disposedValue) - { - try - { - _semaphore?.Release(); - } - finally - { - try - { - _semaphore?.Dispose(); - } - catch { } - } - disposedValue = true; - } + // we do NOT want the finalizer to throw - never ever } + } - // 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 + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); // finalize will not run + } - ~NamedSemaphoreReleaser() + private void Dispose(bool disposing) + { + if (!_disposedValue) { try { - Dispose(false); + _semaphore?.Release(); } - catch + finally { - // we do NOT want the finalizer to throw - never ever + try + { + _semaphore?.Dispose(); + } + catch + { + } } - } - - #endregion + _disposedValue = true; + } } + } - private class SemaphoreSlimReleaser : IDisposable - { - private readonly SemaphoreSlim _semaphore; + private class SemaphoreSlimReleaser : IDisposable + { + private readonly SemaphoreSlim _semaphore; - internal SemaphoreSlimReleaser(SemaphoreSlim semaphore) - { - _semaphore = semaphore; - } + internal SemaphoreSlimReleaser(SemaphoreSlim semaphore) => _semaphore = semaphore; - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + ~SemaphoreSlimReleaser() => Dispose(false); - private void Dispose(bool disposing) - { - if (disposing) - { - // normal - _semaphore.Release(); - } - } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - ~SemaphoreSlimReleaser() + private void Dispose(bool disposing) + { + if (disposing) { - Dispose(false); + // normal + _semaphore.Release(); } } } diff --git a/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs b/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs index 7fd0ee5a85ff..bd41914010f4 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 bb832bfd7e57..23b0d154a48e 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 53dc6d1a6e12..53c07766e833 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 ea6ff63f9199..31bab02f1c95 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 b6e40665c16e..a7b5882ecc73 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; + siteIdentifier = Guid.Empty; + return false; } - /// - 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; + } - siteIdentifier = parsedTelemetryId; + /// + public bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier) + { + if (TryGetSiteIdentifier(out Guid existingId)) + { + siteIdentifier = existingId; return true; } - /// - public bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier) + if (TryCreateSiteIdentifier(out Guid createdId)) { - if (TryGetSiteIdentifier(out Guid existingId)) - { - siteIdentifier = existingId; - return true; - } - - if (TryCreateSiteIdentifier(out Guid createdId)) - { - siteIdentifier = createdId; - return true; - } - - siteIdentifier = Guid.Empty; - return false; + siteIdentifier = createdId; + return true; } - /// - public bool TryCreateSiteIdentifier(out Guid createdGuid) - { - createdGuid = Guid.NewGuid(); + siteIdentifier = Guid.Empty; + return false; + } - 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; - } + /// + public bool TryCreateSiteIdentifier(out Guid createdGuid) + { + createdGuid = Guid.NewGuid(); - return true; + 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 bcc6076d24fc..4ebf1ba0b920 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.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 46ac9fb6e7ac..aa0e9a09bff2 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 - { - public HtmlImageSourceParser(Func getMediaUrl) - { - this._getMediaUrl = getMediaUrl; - } +public sealed class HtmlImageSourceParser +{ + private static readonly Regex ResolveImgPattern = new( + @"(]*src="")([^""\?]*)((?:\?[^""]*)?""[^>]*data-udi="")([^""]*)(""[^>]*>)", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - private readonly IPublishedUrlProvider? _publishedUrlProvider; + private static readonly Regex DataUdiAttributeRegex = new( + @"data-udi=\\?(?:""|')(?umb://[A-z0-9\-]+/[A-z0-9]+)\\?(?:""|')", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - public HtmlImageSourceParser(IPublishedUrlProvider publishedUrlProvider) - { - _publishedUrlProvider = publishedUrlProvider; - } + private readonly IPublishedUrlProvider? _publishedUrlProvider; - private static readonly Regex ResolveImgPattern = new Regex(@"(]*src="")([^""\?]*)((?:\?[^""]*)?""[^>]*data-udi="")([^""]*)(""[^>]*>)", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + private Func? _getMediaUrl; - private static readonly Regex DataUdiAttributeRegex = new Regex(@"data-udi=\\?(?:""|')(?umb://[A-z0-9\-]+/[A-z0-9]+)\\?(?:""|')", - RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + public HtmlImageSourceParser(Func getMediaUrl) => _getMediaUrl = getMediaUrl; - private Func? _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) + /// + /// Parses out media UDIs from an html string based on 'data-udi' html attributes + /// + /// + /// + public IEnumerable FindUdisFromDataAttributes(string text) + { + MatchCollection matches = DataUdiAttributeRegex.Matches(text); + if (matches.Count == 0) { - var matches = DataUdiAttributeRegex.Matches(text); - if (matches.Count == 0) - yield break; + yield break; + } - foreach (Match match in matches) + 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); + _getMediaUrl = guid => _publishedUrlProvider?.GetMediaUrl(guid); + } - return ResolveImgPattern.Replace(text, match => + 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) { - // 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.Value; + } - return $"{match.Groups[1].Value}{mediaUrl}{match.Groups[3].Value}{udi}{match.Groups[5].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; + } - /// - /// 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 $"{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 4317f05cc97b..103070505127 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); - internal static readonly Regex LocalLinkPattern = new Regex(@"href=""[/]?(?:\{|\%7B)localLink:([a-zA-Z0-9-://]+)(?:\}|\%7D)", - RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; - public HtmlLocalLinkParser(IUmbracoContextAccessor umbracoContextAccessor, IPublishedUrlProvider publishedUrlProvider) - { - _umbracoContextAccessor = umbracoContextAccessor; - _publishedUrlProvider = publishedUrlProvider; - } + public HtmlLocalLinkParser( + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedUrlProvider publishedUrlProvider) + { + _umbracoContextAccessor = umbracoContextAccessor; + _publishedUrlProvider = publishedUrlProvider; + } - public IEnumerable FindUdisFromLocalLinks(string text) + public IEnumerable FindUdisFromLocalLinks(string text) + { + foreach ((var intId, GuidUdi? udi, var tagValue) in FindLocalLinkIds(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"); - } + throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); + } - if (!preview) - { - return EnsureInternalLinks(text); - } + if (!preview) + { + return EnsureInternalLinks(text); + } - using (umbracoContext!.ForcedPreview(preview)) // force for URL provider - { - return EnsureInternalLinks(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) + /// + /// Parses the string looking for the {localLink} syntax and updates them to their correct links. + /// + /// + /// + /// + public string EnsureInternalLinks(string text) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out _)) { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out _)) - { - throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); - } + throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); + } - foreach((int? intId, GuidUdi? udi, string tagValue) in FindLocalLinkIds(text)) + 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); + } + + if (newLink == null) + { + newLink = "#"; } - } - return text; + 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); + 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)) + // The id could be an int or a UDI + if (UdiParser.TryParse(id, out Udi? 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); + yield return (null, guidUdi, tag.Value); } + } - if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intId)) - { - yield return (intId, null, 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 39c82f00ab43..f4a817485da4 100644 --- a/src/Umbraco.Core/Templates/HtmlUrlParser.cs +++ b/src/Umbraco.Core/Templates/HtmlUrlParser.cs @@ -5,66 +5,77 @@ 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); + return text; } - /// - /// 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) + using (DisposableTimer? timer = _profilingLogger.DebugDuration( + typeof(IOHelper), + "ResolveUrlsFromTextString starting", + "ResolveUrlsFromTextString complete")) { - 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 ~) + MatchCollection tags = ResolveUrlPattern.Matches(text); + _logger.LogDebug("After regex: {Duration} matched: {TagsCount}", timer?.Stopwatch.ElapsedMilliseconds, tags.Count); + foreach (Match tag in tags) { - // 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 = string.Empty; + if (tag.Groups[1].Success) { - var url = ""; - if (tag.Groups[1].Success) - url = tag.Groups[1].Value; + 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); - } + // 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; } + + return text; } } diff --git a/src/Umbraco.Core/Templates/ITemplateRenderer.cs b/src/Umbraco.Core/Templates/ITemplateRenderer.cs index f6e6435a8a8d..17d16168ec99 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 1239f2287702..b94d575b9faa 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); - - /// - /// Renders the macro with the specified alias. - /// - /// The content id - /// The alias. - Task RenderMacroAsync(int contentId, string alias); + /// 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, 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. + /// + /// 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, 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, object 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. + /// + /// 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); } diff --git a/src/Umbraco.Core/Templates/UmbracoComponentRenderer.cs b/src/Umbraco.Core/Templates/UmbracoComponentRenderer.cs index 407f85ad600e..e419bd5be328 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; - - /// - /// Initializes a new instance of the class. - /// - public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer) - { - _umbracoContextAccessor = umbracoContextAccessor; - _macroRenderer = macroRenderer; - _templateRenderer = templateRenderer ?? throw new ArgumentNullException(nameof(templateRenderer)); - } + _umbracoContextAccessor = umbracoContextAccessor; + _macroRenderer = macroRenderer; + _templateRenderer = templateRenderer ?? throw new ArgumentNullException(nameof(templateRenderer)); + } - /// - public async Task RenderTemplateAsync(int contentId, int? altTemplateId = null) + /// + public async Task RenderTemplateAsync(int contentId, int? altTemplateId = null) + { + using (var sw = new StringWriter()) { - using (var sw = new StringWriter()) + try + { + await _templateRenderer.RenderAsync(contentId, altTemplateId, sw); + } + catch (Exception ex) { - try - { - await _templateRenderer.RenderAsync(contentId, altTemplateId, sw); - } - catch (Exception ex) - { - sw.Write("", contentId, ex); - } - - return new HtmlEncodedString(sw.ToString()); + sw.Write("", contentId, ex); } + + 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) => + 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, object parameters) => + await RenderMacroAsync(contentId, alias, parameters.ToDictionary()); - /// - public async Task RenderMacroAsync(int contentId, string alias, IDictionary? parameters) + /// + public async Task RenderMacroAsync(int contentId, string alias, IDictionary? parameters) + { + if (contentId == default) { - if (contentId == default) - { - throw new ArgumentException("Invalid content id " + contentId); - } - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var content = umbracoContext.Content?.GetById(contentId); + throw new ArgumentException("Invalid content id " + contentId); + } - if (content == null) - { - throw new InvalidOperationException("Cannot render a macro, no content found by id " + contentId); - } + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IPublishedContent? content = umbracoContext.Content?.GetById(contentId); - return await RenderMacroAsync(content, alias, parameters); + if (content == null) + { + throw new InvalidOperationException("Cannot render a macro, no content found by id " + contentId); } - /// - 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); + } - 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"); } - /// - /// Renders the macro with the specified alias, passing in the specified parameters. - /// - private async Task RenderMacroAsync(IPublishedContent content, string alias, IDictionary? parameters) + 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) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } + 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); + // 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; + var html = (await _macroRenderer.RenderAsync(alias, content, macroProps)).Text; - return new HtmlEncodedString(html!); - } + return new HtmlEncodedString(html!); } } diff --git a/src/Umbraco.Core/Tour/BackOfficeTourFilter.cs b/src/Umbraco.Core/Tour/BackOfficeTourFilter.cs index 3fba765f83dd..d1d8384502e6 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 2864abbcede7..44905f912747 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 61f10cc96d9d..b39bcede465e 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 - { - private readonly HashSet _instances = new HashSet(); + public void AddFilter(BackOfficeTourFilter filter) => _instances.Add(filter); - /// - protected override IEnumerable CreateItems(IServiceProvider factory) - { - return base.CreateItems(factory).Concat(_instances); - } + /// + protected override IEnumerable CreateItems(IServiceProvider factory) => + 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 a filter instance. + /// + public void RemoveFilter(BackOfficeTourFilter filter) => _instances.Remove(filter); - /// - /// Removes all filter instances. - /// - public void RemoveAllFilters() - { - _instances.Clear(); - } + /// + /// Removes all filter instances. + /// + public void RemoveAllFilters() => _instances.Clear(); - /// - /// Removes filters matching a condition. - /// - public void RemoveFilter(Func predicate) - { - _instances.RemoveWhere(new Predicate(predicate)); - } + /// + /// 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 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 fcf455c6ad42..c2be2cea5439 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 eedad5b600de..b1c29ccb632c 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 fca82ca18b60..bba1bfc2dce8 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 dd61ba0cdb72..42883d0f8749 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 +namespace Umbraco.Cms.Core.Trees; + +public interface ISearchableTree : IDiscoverable { - public interface ISearchableTree : IDiscoverable - { - /// - /// The alias of the tree that the belongs to - /// - string TreeAlias { get; } + /// + /// 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); - } + /// + /// 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 106b3eef3750..efb3cfab972e 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 66bdba55d41a..aaace2cbd32b 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 -{ - /// - /// A menu item collection for a given tree node - /// - [DataContract(Name = "menuItems", Namespace = "")] - public class MenuItemCollection - { - private readonly MenuItemList _menuItems; +namespace Umbraco.Cms.Core.Trees; - public MenuItemCollection(ActionCollection actionCollection) - { - _menuItems = new MenuItemList(actionCollection); - } +/// +/// 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) - { - _menuItems = new MenuItemList(actionCollection, items); - } + public MenuItemCollection(ActionCollection actionCollection, IEnumerable items) => + Items = 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; } + /// + /// 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 112b8b624026..da24b0d933f7 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 -{ - public class MenuItemCollectionFactory: IMenuItemCollectionFactory - { - private readonly ActionCollection _actionCollection; +namespace Umbraco.Cms.Core.Trees; - public MenuItemCollectionFactory(ActionCollection actionCollection) - { - _actionCollection = actionCollection; - } +public class MenuItemCollectionFactory : IMenuItemCollectionFactory +{ + private readonly ActionCollection _actionCollection; - public MenuItemCollection Create() - { - return new MenuItemCollection(_actionCollection); - } + public MenuItemCollectionFactory(ActionCollection actionCollection) => _actionCollection = actionCollection; - } + public MenuItemCollection Create() => new MenuItemCollection(_actionCollection); } diff --git a/src/Umbraco.Core/Trees/MenuItemList.cs b/src/Umbraco.Core/Trees/MenuItemList.cs index b3fe4206022e..a4cb1899e367 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 - { - var item = _actionCollection.GetAction(); - if (item == null) return null; - - var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture); - values.TryGetValue($"visuallyHiddenTexts/{item.Alias}Description", out var textDescription); + 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)) - { - SeparatorBefore = hasSeparator, - OpensDialog = opensDialog, - TextDescription = 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 33104cb8c7e4..44b0a896acf1 100644 --- a/src/Umbraco.Core/Trees/SearchableApplicationTree.cs +++ b/src/Umbraco.Core/Trees/SearchableApplicationTree.cs @@ -1,22 +1,26 @@ -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +public class SearchableApplicationTree { - public class SearchableApplicationTree + public SearchableApplicationTree(string appAlias, string treeAlias, int sortOrder, string formatterService, string formatterMethod, ISearchableTree searchableTree) { - 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; } + 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 ca5cfef02a26..f3a92fe82f0b 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) + { + } + + /// + /// 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) { - 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) { - 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)); + throw new ArgumentNullException(nameof(serviceName)); + } - ServiceName = serviceName; - MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName)); - SortOrder = sortOrder; + 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 ff42b5e8c381..fdf2c8124b07 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; + private readonly Dictionary _dictionary; - public SearchableTreeCollection(Func> items, ITreeService treeService) - : base(items) - { - _dictionary = CreateDictionary(treeService); - } + public SearchableTreeCollection(Func> items, ITreeService treeService) + : base(items) => + _dictionary = CreateDictionary(treeService); - private Dictionary CreateDictionary(ITreeService treeService) + public IReadOnlyDictionary SearchableApplicationTrees => _dictionary; + + public SearchableApplicationTree this[string key] => _dictionary[key]; + + private Dictionary CreateDictionary(ITreeService treeService) + { + Tree[] appTrees = treeService.GetAll() + .OrderBy(x => x.SortOrder) + .ToArray(); + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + ISearchableTree[] searchableTrees = this.ToArray(); + foreach (Tree appTree in appTrees) { - 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 dca2839558fb..372866ba68a0 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 +namespace Umbraco.Cms.Core.Trees; + +public class SearchableTreeCollectionBuilder : LazyCollectionBuilderBase { - public class SearchableTreeCollectionBuilder : LazyCollectionBuilderBase - { - protected override SearchableTreeCollectionBuilder This => this; + protected override SearchableTreeCollectionBuilder This => this; - //per request because generally an instance of ISearchableTree is a controller - protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Scoped; - } + // 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 f229dd801921..47ee0b234b6a 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; - } + 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; + } - /// - public int SortOrder { get; set; } + /// + /// Gets the tree controller type. + /// + public Type TreeControllerType { get; } - /// - public string SectionAlias { get; set; } + /// + public int SortOrder { get; set; } - /// - public string? TreeGroup { get; } + /// + public string SectionAlias { get; set; } - /// - public string TreeAlias { get; } + /// + public string? TreeGroup { get; } - /// - public string? TreeTitle { get; set; } + /// + public string TreeAlias { get; } - /// - public TreeUse TreeUse { get; set; } + /// + public string? TreeTitle { get; set; } - /// - public bool IsSingleNodeTree { get; } + /// + public TreeUse TreeUse { get; set; } - /// - /// Gets the tree controller type. - /// - public Type TreeControllerType { get; } + /// + public bool IsSingleNodeTree { get; } - public static string? GetRootNodeDisplayName(ITree tree, ILocalizedTextService textService) - { - var label = $"[{tree.TreeAlias}]"; + 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); + // 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 + // 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) { - // the localizedLabel translated into something that's not just [alias], so use the translation - label = localizedLabel; + label = tree.TreeTitle; } - - return label; } + else + { + // the localizedLabel translated into something that's not just [alias], so use the translation + label = localizedLabel; + } + + return label; } } diff --git a/src/Umbraco.Core/Trees/TreeCollection.cs b/src/Umbraco.Core/Trees/TreeCollection.cs index 59fa99819c00..fa6283753ad6 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 +namespace Umbraco.Cms.Core.Trees; + +/// +/// Represents the collection of section trees. +/// +public class TreeCollection : BuilderCollectionBase { - /// - /// Represents the collection of section trees. - /// - public class TreeCollection : BuilderCollectionBase + public TreeCollection(Func> items) + : base(items) { - - public TreeCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Trees/TreeNode.cs b/src/Umbraco.Core/Trees/TreeNode.cs index 3c166c9fdd24..dde66bd3a3c5 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. /// - /// - /// TreeNode is sealed to prevent developers from adding additional json data to the response - /// - [DataContract(Name = "node", Namespace = "")] - public class TreeNode : EntityBasic + /// + /// The parent id for the current node + /// + /// + public TreeNode(string nodeId, string? parentId, string? getChildNodesUrl, string? menuUrl) { - /// - /// 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) { - 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"; + throw new ArgumentNullException(nameof(nodeId)); } - [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 + if (string.IsNullOrWhiteSpace(nodeId)) { - get - { - if (Icon.IsNullOrWhiteSpace()) - { - return true; - } + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(nodeId)); + } - if (Icon!.StartsWith("..")) - return false; + Id = nodeId; + ParentId = parentId; + ChildNodesUrl = getChildNodesUrl; + MenuUrl = menuUrl; + CssClasses = new List(); + // default + Icon = "icon-folder-close"; + Path = "-1"; + } - //if it starts with a '.' or doesn't contain a '.' at all then it is a class - return Icon.StartsWith(".") || Icon.Contains(".") == false; - } - } + [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 the icon file path if the icon is not a class, otherwise returns an empty string - /// - [DataMember(Name = "iconFilePath")] - public string IconFilePath + /// + /// Returns true if the icon represents a CSS class instead of a file path + /// + [DataMember(Name = "iconIsClass")] + public bool IconIsClass + { + get { - get + if (Icon.IsNullOrWhiteSpace()) { - 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 true; + } + + if (Icon!.StartsWith("..")) + { + 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 545b6881aaa6..b76fcc41ce8e 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 9e887f68ec85..7fdc8ef480ed 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"; + 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) + /// + /// Sets the node style to show that it is a container type + /// + /// + public static void SetContainerStyle(this TreeNode treeNode) + { + 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) - { - if (treeNode.CssClasses.Contains("is-container") == false) - { - treeNode.CssClasses.Add("is-container"); - } - } + /// + /// 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) + /// + /// 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("protected") == false) - { - treeNode.CssClasses.Add("protected"); - } + treeNode.CssClasses.Add("protected"); } + } - /// - /// 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 currently locked / non-deletable + /// + /// + public static void SetLockedStyle(this TreeNode treeNode) + { + if (treeNode.CssClasses.Contains("locked") == false) { - if (treeNode.CssClasses.Contains("locked") == false) - { - treeNode.CssClasses.Add("locked"); - } + treeNode.CssClasses.Add("locked"); } + } - /// - /// 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 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("has-unpublished-version") == false) - { - treeNode.CssClasses.Add("has-unpublished-version"); - } + treeNode.CssClasses.Add("has-unpublished-version"); } + } - /// - /// Sets the node style to show that it is not published - /// - /// - public static void SetNotPublishedStyle(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("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 55be24d54db9..ff06bc1dead6 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 2e141e2e6686..bc6c1ab6ac34 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 +{ /// - /// 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 entity type part of the identifier. + /// The string value of the identifier. + protected Udi(string entityType, string stringValue) { - 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); - } + 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; - } + /// + /// Initializes a new instance of the Udi class. + /// + /// The uri value of the identifier. + protected Udi(Uri uriValue) + { + EntityType = uriValue.Host; + UriValue = uriValue; + } + public Uri UriValue { get; } + /// + /// Gets the entity type part of the identifier. + /// + public string EntityType { get; } - /// - /// Gets the entity type part of the identifier. - /// - public string EntityType { get; private set; } + /// + /// 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 int CompareTo(Udi? other) + public static bool operator ==(Udi? udi1, Udi? udi2) + { + if (ReferenceEquals(udi1, udi2)) { - return string.Compare(UriValue.ToString(), other?.UriValue.ToString(), StringComparison.OrdinalIgnoreCase); + return true; } - public override string ToString() + if (udi1 is null || udi2 is null) { - // UriValue is created in the ctor and is never null - // use AbsoluteUri here and not ToString else it's not encoded! - return UriValue.AbsoluteUri; + 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); - /// - /// 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); - } + public int CompareTo(Udi? other) => string.Compare(UriValue.ToString(), other?.UriValue.ToString(), StringComparison.OrdinalIgnoreCase); - /// - /// 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"); + public override string ToString() => - 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)); + // UriValue is created in the ctor and is never null + // use AbsoluteUri here and not ToString else it's not encoded! + UriValue.AbsoluteUri; - return new StringUdi(entityType, id); + /// + /// 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"); } - /// - /// 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 (string.IsNullOrWhiteSpace(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); + throw new ArgumentException("Value cannot be null or whitespace.", "id"); } - public static Udi Create(Uri uri) + if (udiType != UdiType.StringUdi) { - // if it's a know type go fast and use ctors - // else fallback to parsing the string (and guess the type) + throw new InvalidOperationException(string.Format( + "Entity type \"{0}\" does not have string udis.", + entityType)); + } - if (UdiParser.UdiTypes.TryGetValue(uri.Host, out var udiType) == false) - throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", uri.Host), "uri"); + return new StringUdi(entityType, id); + } - if (udiType == UdiType.GuidUdi) - return new GuidUdi(uri); - if (udiType == UdiType.StringUdi) - return new StringUdi(uri); + /// + /// 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"); + } - throw new ArgumentException(string.Format("Uri \"{0}\" is not a valid udi.", uri)); + if (udiType != UdiType.GuidUdi) + { + throw new InvalidOperationException(string.Format( + "Entity type \"{0}\" does not have guid udis.", + entityType)); } - public void EnsureType(params string[] validTypes) + if (id == default) { - if (validTypes.Contains(EntityType) == false) - throw new Exception(string.Format("Unexpected entity type \"{0}\".", EntityType)); + throw new ArgumentException("Cannot be an empty guid.", "id"); } - /// - /// 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() + 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) { - if (IsRoot) throw new Exception("Root Udi."); - return this; + throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", uri.Host), "uri"); } - public override bool Equals(object? obj) + if (udiType == UdiType.GuidUdi) { - var other = obj as Udi; - return other is not null && GetType() == other.GetType() && UriValue == other.UriValue; + return new GuidUdi(uri); } - public override int GetHashCode() + if (udiType == UdiType.StringUdi) { - return UriValue.GetHashCode(); + return new StringUdi(uri); } - public static bool operator ==(Udi? udi1, Udi? udi2) + throw new ArgumentException(string.Format("Uri \"{0}\" is not a valid udi.", uri)); + } + + public void EnsureType(params string[] validTypes) + { + if (validTypes.Contains(EntityType) == false) { - if (ReferenceEquals(udi1, udi2)) return true; - if ((object?)udi1 == null || (object?)udi2 == null) return false; - return udi1.Equals(udi2); + throw new Exception(string.Format("Unexpected entity type \"{0}\".", EntityType)); } + } - public static bool operator !=(Udi? udi1, Udi? udi2) + /// + /// Ensures that this Udi is not a root Udi. + /// + /// This Udi. + /// When this Udi is a Root Udi. + public Udi EnsureNotRoot() + { + if (IsRoot) { - return (udi1 == udi2) == false; + 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 9139ef418834..fe96909f78b5 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 781c084785de..f0e8774cf89b 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 907880db1357..30448e1b4543 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 ConcurrentDictionary(); - internal static ConcurrentDictionary UdiTypes { get; private set; } + 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()); - } + static UdiParser() => - /// - /// 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()); - } + // initialize with known (built-in) Udi types + // we will add scanned types later on + 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) - { - ParseInternal(s, false, false, out var udi); - return udi!; - } + internal static ConcurrentDictionary UdiTypes { get; private set; } - /// - /// 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!; - } + /// + /// 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) + { + ParseInternal(s, false, false, 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(returnValue: false)] out Udi 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 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) { - return ParseInternal(s, true, false, out udi); + udi = (T)parsed; + return true; } - /// - /// 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? + 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(false)] out Udi udi) => + ParseInternal(s, true, knownTypes, out udi); + + /// + /// Registers a custom entity type. + /// + /// + /// + public static void RegisterUdiType(string entityType, UdiType udiType) => UdiTypes.TryAdd(entityType, udiType); + + internal static Udi GetRootUdi(string entityType) => + RootUdis.GetOrAdd(entityType, x => { - 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; - return true; + throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType)); } - udi = null; - return false; - } + return udiType == UdiType.StringUdi + ? new StringUdi(entityType, string.Empty) + : new GuidUdi(entityType, Guid.Empty); + }); - /// - /// 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) + 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) { - return ParseInternal(s, true, knownTypes, out udi); + if (tryParse) + { + return false; + } + + throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); } - private static bool ParseInternal(string? s, bool tryParse, bool knownTypes,[MaybeNullWhen(returnValue: false)] out Udi udi) + var entityType = uri.Host; + if (UdiTypes.TryGetValue(entityType, out UdiType udiType) == false) { - udi = null; - if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false - || Uri.TryCreate(s, UriKind.Absolute, out var uri) == false) + if (knownTypes) { - if (tryParse) return false; - throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); + // not knowing the type is not an error + // just return the unknown type udi + udi = UnknownTypeUdi.Instance; + return false; } - var entityType = uri.Host; - if (UdiTypes.TryGetValue(entityType, out var udiType) == false) + if (tryParse) { - 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)); + return false; } - var path = uri.AbsolutePath.TrimStart('/'); + throw new FormatException(string.Format("Unknown entity type \"{0}\".", entityType)); + } + + var path = uri.AbsolutePath.TrimStart('/'); - if (udiType == UdiType.GuidUdi) + if (udiType == UdiType.GuidUdi) + { + if (path == string.Empty) { - 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); + udi = GetRootUdi(uri.Host); return true; } - if (udiType == UdiType.StringUdi) + if (Guid.TryParse(path, out Guid guid) == false) { - udi = path == string.Empty ? GetRootUdi(uri.Host) : new StringUdi(uri.Host, Uri.UnescapeDataString(path)); - return true; + if (tryParse) + { + return false; + } + + throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); } - 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 320cc9a90193..4c307435dec9 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 - { - // 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 readonly object ScanLocker = new(); - private static volatile bool _scanned; - private static readonly object ScanLocker = new object(); + // 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) + /// + /// 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)); + throw new ArgumentNullException(nameof(typeLoader)); + } - if (_scanned) return; + if (_scanned) + { + return; + } - lock (ScanLocker) + 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(); + foreach (Type connector in connectors) { - // 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) + IEnumerable + attrs = connector.GetCustomAttributes(false); + foreach (UdiDefinitionAttribute attr in attrs) { - var attrs = connector.GetCustomAttributes(false); - foreach (var attr in attrs) + if (result.TryGetValue(attr.EntityType, out UdiType udiType) && udiType != attr.UdiType) { - 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; + throw new Exception(string.Format( + "Entity type \"{0}\" is declared by more than one IServiceConnector, with different UdiTypes.", + attr.EntityType)); } - } - // merge these into the known list - foreach (var item in result) - UdiParser.RegisterUdiType(item.Key, item.Value); + result[attr.EntityType] = attr.UdiType; + } + } - _scanned = true; + // merge these into the known list + 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 + /// + /// 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) { - var result = new Dictionary(); - var connector = typeof(T); - var attrs = connector.GetCustomAttributes(false); - foreach (var attr in attrs) + if (result.TryGetValue(attr.EntityType, out UdiType udiType) && udiType != attr.UdiType) { - 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; + throw new Exception(string.Format( + "Entity type \"{0}\" is declared by more than one IServiceConnector, with different UdiTypes.", + attr.EntityType)); } - // merge these into the known list - foreach (var item in result) - UdiParser.RegisterUdiType(item.Key, item.Value); + 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 ca5b07bf36db..5d98664a3e6f 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)); - } + 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 the for this range. + /// + public Udi Udi { get; } - /// - /// Gets or sets the selector for this range. - /// - public string Selector { get; private set; } + /// + /// Gets or sets the selector for this range. + /// + public string Selector { get; } - /// - /// Gets the entity type of the for this range. - /// - public string EntityType - { - get { return Udi.EntityType; } - } + /// + /// Gets the entity type of the for this range. + /// + public string EntityType => Udi.EntityType; - public static UdiRange Parse(string s) + public static bool operator ==(UdiRange? range1, UdiRange? range2) + { + if (ReferenceEquals(range1, range2)) { - 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)); + return true; } - public override string ToString() + if (range1 is null || range2 is null) { - return _uriValue.ToString(); + return false; } - public override bool Equals(object? obj) - { - return obj is UdiRange other && GetType() == other.GetType() && _uriValue == other._uriValue; - } + return range1.Equals(range2); + } - public override int GetHashCode() - { - return _uriValue.GetHashCode(); - } + public static bool operator !=(UdiRange range1, UdiRange range2) => !(range1 == range2); - public static bool operator ==(UdiRange range1, UdiRange range2) + public static UdiRange Parse(string s) + { + if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false + || Uri.TryCreate(s, UriKind.Absolute, out Uri? uri) == false) { - if (ReferenceEquals(range1, range2)) return true; - if ((object)range1 == null || (object)range2 == null) return false; - return range1.Equals(range2); + // if (tryParse) return false; + throw new FormatException(string.Format("String \"{0}\" is not a valid udi range.", s)); } - public static bool operator !=(UdiRange range1, UdiRange range2) - { - return !(range1 == range2); - } + 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 572c36de952f..e5ebd2f7ce04 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 c443b1817b11..2a52a1e09361 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 66ad608881b1..afd6183b548a 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 89959c3b3266..d17012e0f98c 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) - { - IsRoot = isRoot; + UmbracoContext = umbracoContext; + _umbracoContextAccessor = umbracoContextAccessor; + } - UmbracoContext = umbracoContext; - _umbracoContextAccessor = umbracoContextAccessor; - } + /// + /// Gets the . + /// + public IUmbracoContext UmbracoContext { get; } - /// - /// Gets the . - /// - public IUmbracoContext UmbracoContext { get; } + /// + /// Gets a value indicating whether the reference is a root reference. + /// + public bool IsRoot { 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) + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - 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 4131eae05338..3c38418f0e84 100644 --- a/src/Umbraco.Core/UnknownTypeUdi.cs +++ b/src/Umbraco.Core/UnknownTypeUdi.cs @@ -1,16 +1,13 @@ -namespace Umbraco.Cms.Core -{ - public class UnknownTypeUdi : Udi - { - private UnknownTypeUdi() - : base("unknown", "umb://unknown/") - { } +namespace Umbraco.Cms.Core; - public static readonly UnknownTypeUdi Instance = new UnknownTypeUdi(); +public class UnknownTypeUdi : Udi +{ + public static readonly UnknownTypeUdi Instance = new(); - public override bool IsRoot - { - get { return false; } - } + private UnknownTypeUdi() + : base("unknown", "umb://unknown/") + { } + + public override bool IsRoot => false; } diff --git a/src/Umbraco.Core/UpgradeResult.cs b/src/Umbraco.Core/UpgradeResult.cs index 25431a59836b..7f27e503fe42 100644 --- a/src/Umbraco.Core/UpgradeResult.cs +++ b/src/Umbraco.Core/UpgradeResult.cs @@ -1,16 +1,17 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public class UpgradeResult { - public class UpgradeResult + public UpgradeResult(string upgradeType, string comment, string upgradeUrl) { - public string UpgradeType { get; } - public string Comment { get; } - public string UpgradeUrl { get; } - - public UpgradeResult(string upgradeType, string comment, string upgradeUrl) - { - UpgradeType = upgradeType; - Comment = comment; - UpgradeUrl = 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 68b6234a0fb2..3599a7c16a5c 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 -{ - public static class UriUtilityCore - { +namespace Umbraco.Cms.Core; - #region Uri string utilities +public static class UriUtilityCore +{ + #region Uri string utilities - public static bool HasScheme(string uri) - { - return uri.IndexOf("://") > 0; - } + public static bool HasScheme(string uri) => uri.IndexOf("://") > 0; - public static string StartWithScheme(string uri) - { - return StartWithScheme(uri, null); - } + public static string StartWithScheme(string uri) => 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 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); - - var path = pos > 0 ? uri.Substring(0, pos) : uri; - path = path.EnsureEndsWith('/'); + 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); - if (pos > 0) - path += uri.Substring(pos); + var path = pos > 0 ? uri.Substring(0, pos) : uri; + path = path.EnsureEndsWith('/'); - return path; + if (pos > 0) + { + path += uri.Substring(pos); } - 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); + return path; + } - var path = pos > 0 ? uri.Substring(0, pos) : uri; - path = path.TrimEnd(Constants.CharArrays.ForwardSlash); + 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); - if (pos > 0) - path += uri.Substring(pos); + var path = pos > 0 ? uri[..pos] : uri; + path = path.TrimEnd(Constants.CharArrays.ForwardSlash); - return path; + if (pos > 0) + { + path += uri.Substring(pos); } - #endregion - + return path; } + + #endregion } diff --git a/src/Umbraco.Core/Web/CookieManagerExtensions.cs b/src/Umbraco.Core/Web/CookieManagerExtensions.cs index 75014000bb96..2e399ac8c1f9 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 -{ - public static class CookieManagerExtensions - { - public static string? GetPreviewCookieValue(this ICookieManager cookieManager) - { - return cookieManager.GetCookieValue(Constants.Web.PreviewCookieName); - } - - } +namespace Umbraco.Extensions; +public static class CookieManagerExtensions +{ + 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 94710429f079..509a746b3085 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; + /// + /// Tries to get the object. + /// + public bool TryGetUmbracoContext([MaybeNullWhen(false)] out IUmbracoContext umbracoContext) + { + umbracoContext = Value; - return umbracoContext is not null; - } + return umbracoContext is not null; + } - /// - /// Clears the current object. - /// - public void Clear() => Value = null; + /// + /// Clears the current object. + /// + public void Clear() => Value = null; - /// - /// Sets the object. - /// - /// - public void Set(IUmbracoContext umbracoContext) => Value = umbracoContext; - } + /// + /// 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 730b78a705f6..2815675f5d17 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); + + string? GetCookieValue(string cookieName); - public interface ICookieManager - { - void ExpireCookie(string cookieName); - string? GetCookieValue(string cookieName); - void SetCookieValue(string cookieName, string value); - bool HasCookie(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 9fb4e99d5c75..a72ec5bc72e4 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 3ba691e22208..a37bebcfa780 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 7cfa3876c0e7..17ffc515a22c 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 d8e6793f8932..370412b2814a 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 68ebcf8b2bd6..d8d5475841d1 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 efc162a9a3b4..5f484c8fe027 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 { + public Type ControllerType { get; set; } = null!; + + public string? ControllerName { get; set; } + + public string? ControllerNamespace { get; set; } + + public string? AreaName { get; set; } + /// - /// Represents some metadata about the controller + /// 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 class PluginControllerMetadata - { - public Type ControllerType { get; set; } = null!; - 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; } - } + public bool IsBackOffice { get; set; } } diff --git a/src/Umbraco.Core/WebAssets/AssetFile.cs b/src/Umbraco.Core/WebAssets/AssetFile.cs index c10a423a9946..a0ad29830279 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; + + #region IAssetFile Members - public string? FilePath { get; set; } - public AssetType DependencyType { get; } + public string? FilePath { get; set; } - #endregion + public AssetType DependencyType { get; } - public AssetFile(AssetType type) - { - DependencyType = type; - } - } + #endregion } diff --git a/src/Umbraco.Core/WebAssets/AssetType.cs b/src/Umbraco.Core/WebAssets/AssetType.cs index f40a592588e0..e04caa80a2b1 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 64b9e72e17d2..99236494f309 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 101ff22763d5..9ba30c83de49 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 2595afe40e42..523b186c9a26 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 df84bf013d1e..bdfebf128a63 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 dd66afe4a712..f3e5516f45e7 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 baf9549562e0..c6116e122fcc 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 2dccbf2a072d..e7f4ea239fcc 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 7547b7cc31d9..fd8686634893 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 @@ public DynamicContext(XmlNamespaceManager context, NameTable table) 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 @@ public DynamicContext(XmlNamespaceManager context, NameTable table) /// /// 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 @@ public override bool Whitespace /// 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 @@ public override IXsltContextVariable ResolveVariable(string prefix, string name) { IXsltContextVariable var; _variables.TryGetValue(name, out var!); - return var!; + return var; } #endregion Variable Handling Code @@ -215,8 +224,8 @@ public override IXsltContextVariable ResolveVariable(string prefix, string name) /// 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 @@ public DynamicVariable(string name, object value) _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 @@ XPathResultType IXsltContextVariable.VariableType 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 4285f9c97f58..bb5c186ca68a 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)); + } + + if (string.IsNullOrWhiteSpace(xpathExpression)) { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + 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 (getPath == null) + { + throw new ArgumentNullException(nameof(getPath)); + } - 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 (publishedContentExists == null) + { + throw new ArgumentNullException(nameof(publishedContentExists)); + } - //no need to parse it - if (xpathExpression.StartsWith("$") == false) - return xpathExpression; + // no need to parse it + if (xpathExpression.StartsWith("$") == false) + { + return xpathExpression; + } - //get nearest published item - Func?, int> getClosestPublishedAncestor = path => + // get nearest published item + Func?, int> getClosestPublishedAncestor = path => + { + if (path is not null) { - if (path is not null) + foreach (var i in path) { - foreach (var i in path) + if (int.TryParse(i, NumberStyles.Integer, CultureInfo.InvariantCulture, out int idAsInt)) { - int idAsInt; - if (int.TryParse(i, NumberStyles.Integer, CultureInfo.InvariantCulture, out idAsInt)) + var exists = publishedContentExists(int.Parse(i, CultureInfo.InvariantCulture)); + if (exists) { - var exists = publishedContentExists(int.Parse(i, CultureInfo.InvariantCulture)); - if (exists) - return idAsInt; + return idAsInt; } } } + } - return -1; - }; + return -1; + }; - const string rootXpath = "id({0})"; + const string rootXpath = "id({0})"; - //parseable items: - var vars = new Dictionary>(); + // parseable items: + var vars = new Dictionary>(); - //These parameters must have a node id context - if (nodeContextId.HasValue) + // These parameters must have a node id context + if (nodeContextId.HasValue) + { + vars.Add("$current", q => { - vars.Add("$current", q => - { - var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); - return q.Replace("$current", string.Format(rootXpath, closestPublishedAncestorId)); - }); + var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); + return q.Replace("$current", string.Format(rootXpath, closestPublishedAncestorId)); + }); - vars.Add("$parent", q => + 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()) { - //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)); - }); - + path = path?.Skip(1).ToArray(); + } - vars.Add("$site", q => - { - var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); - return q.Replace("$site", string.Format(rootXpath, closestPublishedAncestorId) + "/ancestor-or-self::*[@level = 1]"); - }); - } + var closestPublishedAncestorId = getClosestPublishedAncestor(path); + return q.Replace("$parent", string.Format(rootXpath, closestPublishedAncestorId)); + }); - // 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")); + 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) + foreach (KeyValuePair> varible in vars) + { + if (xpathExpression.StartsWith(varible.Key)) { - if (xpathExpression.StartsWith(varible.Key)) - { - xpathExpression = varible.Value(xpathExpression); - break; - } + xpathExpression = varible.Value(xpathExpression); + break; } - - return xpathExpression; } + return xpathExpression; } } diff --git a/src/Umbraco.Core/Xml/XPath/INavigableContent.cs b/src/Umbraco.Core/Xml/XPath/INavigableContent.cs index c1a4e6c3e464..b9359b4feff0 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 2e214d5e9a3d..08a7c1a0f6cb 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 0b66cc0626d7..28fa46e84bb1 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 76b43b618c9d..1f8500725bd7 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 2e2819066bf5..dd27e6124c51 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 @@ private static int GetUid() #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 @@ void DebugEnter(string name) } [Conditional("DEBUG")] - void DebugCreate(MacroNavigator nav) + private void DebugCreate(MacroNavigator nav) { #if DEBUG Debug("Create: [MacroNavigator::{0}]", nav._uid); @@ -103,16 +100,19 @@ private void DebugReturn(bool value) } [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 @@ void DebugState(string s = " =>") 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 @@ void DebugState(string s = " =>") } #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 @@ public MacroParameter(string name, string value) 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 @@ public MacroParameter(string name, XPathNavigator navigator, 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 @@ public override bool IsEmptyElement 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 @@ public override bool IsEmptyElement var s = _macro.Parameters[_state.ParameterIndex].StringValue; isEmpty = s == null; } + break; case StatePosition.ParameterNavigator: isEmpty = _state.ParameterNavigator?.IsEmptyElement ?? true; @@ -410,7 +419,11 @@ public override bool MoveToFirstAttribute() succ = true; DebugState(); } - else succ = false; + else + { + succ = false; + } + break; case StatePosition.ParameterAttribute: case StatePosition.ParameterNodes: @@ -452,8 +465,8 @@ public override bool MoveToFirstChild() } 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 @@ public override bool MoveToFirstChild() 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 @@ public override bool MoveToFirstChild() succ = true; DebugState(); } - else succ = false; + else + { + succ = false; + } + break; case StatePosition.ParameterAttribute: case StatePosition.ParameterText: @@ -692,7 +713,9 @@ public override bool MoveToNextAttribute() break; case StatePosition.ParameterAttribute: if (_state.ParameterAttributeIndex == _macro.Parameters[_state.ParameterIndex].Attributes?.Length - 1) + { succ = false; + } else { ++_state.ParameterAttributeIndex; @@ -914,7 +937,9 @@ public override string Value 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 @@ private static bool IsDoc(XPathNavigator? nav) 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 @@ internal enum StatePosition 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 a575ee86f8f1..3529f559229b 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. - - 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) + _source = source; + _lastAttributeIndex = source.LastAttributeIndex; + _maxDepth = 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; - //} - - /// - /// 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) - { - _nameTable = orig._nameTable; + InternalState = new State(content, null, null, 0, StatePosition.Root); + + _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; + // } - _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; + /// + /// 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; - _contents = orig._contents; + InternalState = state ?? orig.InternalState.Clone(); + if (state != null && maxDepth < 0) + { + 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 @@ private static int GetUid() } #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 @@ void DebugState(string s = " =>") Debug("State{0} {1}", s, position); #endif - } + } #if DEBUGNAVIGATOR void Debug(string format, params object[] args) @@ -257,980 +257,1035 @@ void Debug(string format, params object[] args) } #endif - #endregion + #endregion - #region Source management + #region Source management - private readonly ConcurrentDictionary _contents; + private readonly ConcurrentDictionary _contents; - private INavigableContent? SourceGet(int id) - { - // original version, would keep creating INavigableContent objects - //return _source.Get(id); + private INavigableContent? SourceGet(int id) => - // improved version, uses a cache, shared with clones - return _contents.GetOrAdd(id, x => _source.Get(x)); - } + // 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 + #endregion - /// - /// Gets the underlying content object. - /// - public override object? UnderlyingObject => _state.Content; + /// + /// 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 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) + /// + /// 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) { - 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); + throw new ArgumentException("Not a valid identifier.", nameof(id)); } - /// - /// 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); - } - } + return CloneWithNewRoot(id); + } - NavigableNavigator? clone = 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(int id, int maxDepth = int.MaxValue) + { + DebugEnter("CloneWithNewRoot"); - if (state != null) - { - clone = new NavigableNavigator(this, state, maxDepth); - DebugCreate(clone); - DebugReturn("[XPathNavigator]"); - } - else - { - DebugReturn("[null]"); - } + State? state = null; - return clone; + if (id <= 0) + { + state = new State(_source.Root, null, null, 0, StatePosition.Root); } - - /// - /// Gets a value indicating whether the current node is an empty element without an end element tag. - /// - public override bool IsEmptyElement + else { - get + INavigableContent? content = SourceGet(id); + if (content != null) { - DebugEnter("IsEmptyElement"); - bool isEmpty; + state = new State(content, null, null, 0, StatePosition.Root); + } + } - 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."); - } + NavigableNavigator? clone = null; - DebugReturn(isEmpty); - return isEmpty; - } + if (state != null) + { + clone = new NavigableNavigator(this, state, maxDepth); + DebugCreate(clone); + DebugReturn("[XPathNavigator]"); + } + else + { + DebugReturn("[null]"); } - /// - /// 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) + 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("IsSamePosition"); - bool isSame; + DebugEnter("IsEmptyElement"); + bool isEmpty; - 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); - break; + throw new InvalidOperationException("Not an element."); default: throw new InvalidOperationException("Invalid position."); } - DebugReturn(isSame); - return isSame; + DebugReturn(isEmpty); + return isEmpty; } + } - /// - /// Gets the qualified name of the current node. - /// - public override string Name - { - 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; - } - } + /// + /// 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; - /// - /// Gets the Name of the current node without any namespace prefix. - /// - public override string LocalName + switch (InternalState.Position) { - get - { - DebugEnter("LocalName"); - var name = Name; - DebugReturn("\"{0}\"", name); - return name; - } + 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."); } - /// - /// 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(); - DebugState(); - succ = true; - } - - DebugReturn(succ); - return succ; - } + DebugReturn(isSame); + return isSame; + } - /// - /// 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() + /// + /// Gets the qualified name of the current node. + /// + public override string Name + { + get { - DebugEnter("MoveToFirstAttribute"); - bool succ; + DebugEnter("Name"); + string name; - switch (_state.Position) + switch (InternalState.Position) { case StatePosition.PropertyXml: - succ = _state.XmlFragmentNavigator?.MoveToFirstAttribute() ?? false; - break; - case StatePosition.Element: - _state.FieldIndex = -1; - _state.Position = StatePosition.Attribute; - DebugState(); - succ = true; + 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: - succ = false; + name = string.Empty; break; default: throw new InvalidOperationException("Invalid position."); } - DebugReturn(succ); - return succ; + DebugReturn("\"{0}\"", name); + return name; } + } - /// - /// 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() + /// + /// Gets the Name of the current node without any namespace prefix. + /// + public override string LocalName + { + get { - DebugEnter("MoveToFirstChild"); - bool succ; + DebugEnter("LocalName"); + var name = Name; + DebugReturn("\"{0}\"", name); + return name; + } + } - 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."); - } + /// + /// 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; - DebugReturn(succ); - return succ; + if (other != null && other._source == _source) + { + InternalState = other.InternalState.Clone(); + DebugState(); + succ = true; } - private bool MoveToFirstChildElement() + 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) { - var children = _state.GetContentChildIds(_maxDepth); + case StatePosition.PropertyXml: + succ = InternalState.XmlFragmentNavigator?.MoveToFirstAttribute() ?? false; + break; + case StatePosition.Element: + InternalState.FieldIndex = -1; + InternalState.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."); + } - 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) + 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) + { + 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; + succ = true; + } + else + { + succ = MoveToFirstChildElement(); } - } - return false; + break; + case StatePosition.PropertyElement: + succ = MoveToFirstChildProperty(); + break; + case StatePosition.Root: + InternalState.Position = StatePosition.Element; + DebugState(); + succ = true; + break; + default: + throw new InvalidOperationException("Invalid position."); } - private bool MoveToFirstChildProperty() - { - var valueForXPath = _state.Content?.Value(_state.FieldIndex); + DebugReturn(succ); + return succ; + } - // value should be - // - an XPathNavigator over a non-empty XML fragment - // - a non-Xml-whitespace string - // - null + private bool MoveToFirstChildElement() + { + IList children = InternalState.GetContentChildIds(_maxDepth); - var nav = valueForXPath as XPathNavigator; - if (nav != null) + 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. + INavigableContent? child = children.Select(id => SourceGet(id)).FirstOrDefault(c => c != null); + if (child != null) { - nav = nav.Clone(); // never use the one we got - nav.MoveToFirstChild(); - _state.XmlFragmentNavigator = nav; - _state.Position = StatePosition.PropertyXml; + InternalState.Position = StatePosition.Element; + InternalState.FieldIndex = -1; + InternalState = new State(child, InternalState, children, 0, StatePosition.Element); DebugState(); return true; } + } - if (valueForXPath == null) - return false; - - if (valueForXPath is string) - { - _state.Position = StatePosition.PropertyText; - DebugState(); - return true; - } + return false; + } - throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string."); + 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; } - /// - /// 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) + if (valueForXPath == null) { - 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) + if (valueForXPath is string) { - DebugEnter("MoveToNextNamespace"); - DebugReturn(false); - return false; + InternalState.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 + State state = InternalState; + + // root state has no parent + while (state.Parent != null) + { + state = state.Parent; } - /// - /// 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) + var navRootId = state.Content?.Id; + + int contentId; + if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out contentId)) { - 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) { - if (contentId == navRootId) - { - _state = new State(state.Content, null, null, 0, StatePosition.Element); - succ = true; - } - else + InternalState = new State(state.Content, null, null, 0, StatePosition.Element); + succ = true; + } + else + { + INavigableContent? content = SourceGet(contentId); + if (content != null) { - 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) { - // 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); - } + s.Push(content); + content = SourceGet(content.ParentId); + } - if (content != null && s.Count < _maxDepth) + if (content != null && s.Count < _maxDepth) + { + InternalState = new State(state.Content, null, null, 0, StatePosition.Element); + while (content != null) { - _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; + 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; } } } - - DebugReturn(succ); - return succ; } - /// - /// 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; + DebugReturn(succ); + return succ; + } - switch (_state.Position) - { - 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; + /// + /// 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; - _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 + switch (InternalState.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) + { + // 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) { - ++_state.FieldIndex; - DebugState(); - succ = true; + continue; } + + InternalState.Content = node; + DebugState(); + succ = true; break; - case StatePosition.PropertyText: - case StatePosition.Attribute: - case StatePosition.Root: - succ = false; - break; - default: - throw new InvalidOperationException("Invalid position."); - } + } + + 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; + } - DebugReturn(succ); - return succ; + break; + case StatePosition.PropertyText: + case StatePosition.Attribute: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); } - /// - /// 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; + DebugReturn(succ); + return 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; + /// + /// 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; - _state.Content = content; - DebugState(); - succ = true; - break; - } - if (succ == false && _state.SiblingIndex == 0 && _state.FieldsCount - 1 > _lastAttributeIndex) + 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) { - // before children elements may come some property elements - if (MoveToParentElement()) // pops the state - { - _state.FieldIndex = _state.FieldsCount - 1; - DebugState(); - succ = true; - } + continue; } + + InternalState.Content = content; + DebugState(); + succ = true; break; - case StatePosition.PropertyElement: - succ = false; - if (_state.FieldIndex > _lastAttributeIndex) + } + + if (succ == false && InternalState.SiblingIndex == 0 && + InternalState.FieldsCount - 1 > _lastAttributeIndex) + { + // before children elements may come some property elements + // pops the state + if (MoveToParentElement()) { - --_state.FieldIndex; + InternalState.FieldIndex = InternalState.FieldsCount - 1; DebugState(); succ = true; } - break; - case StatePosition.Attribute: - case StatePosition.PropertyText: - case StatePosition.Root: + } + + 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; - break; - default: - throw new InvalidOperationException("Invalid position."); - } + } + else + { + ++InternalState.FieldIndex; + DebugState(); + succ = true; + } - DebugReturn(succ); - return succ; + break; + case StatePosition.Element: + case StatePosition.PropertyElement: + case StatePosition.PropertyText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); } - /// - /// 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() + 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) { - DebugEnter("MoveToNextAttribute"); - bool succ; + 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; + } - switch (_state.Position) + 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: - succ = _state.XmlFragmentNavigator?.MoveToNextAttribute() ?? false; + type = InternalState.XmlFragmentNavigator?.NodeType ?? XPathNodeType.Root; break; case StatePosition.Attribute: - if (_state.FieldIndex == _lastAttributeIndex) - succ = false; - else - { - ++_state.FieldIndex; - DebugState(); - succ = true; - } + type = XPathNodeType.Attribute; break; case StatePosition.Element: case StatePosition.PropertyElement: + type = XPathNodeType.Element; + break; case StatePosition.PropertyText: + type = XPathNodeType.Text; + break; case StatePosition.Root: - succ = false; + type = XPathNodeType.Root; break; default: throw new InvalidOperationException("Invalid position."); } - DebugReturn(succ); - return succ; + DebugReturn("\'{0}\'", type); + return type; } + } - /// - /// 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() + /// + /// 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("MoveToParent"); - bool succ; + DebugEnter("Value"); + string value; - switch (_state.Position) + switch (InternalState.Position) { + case StatePosition.PropertyXml: + value = InternalState.XmlFragmentNavigator?.Value ?? string.Empty; + break; case StatePosition.Attribute: + case StatePosition.PropertyText: case StatePosition.PropertyElement: - _state.Position = StatePosition.Element; - _state.FieldIndex = -1; - DebugState(); - succ = true; - break; - case StatePosition.Element: - succ = MoveToParentElement(); - if (succ == false) + if (InternalState.FieldIndex == -1) { - _state.Position = StatePosition.Root; - succ = true; + value = InternalState.Content?.Id.ToString(CultureInfo.InvariantCulture) ?? string.Empty; } - 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) + else { - _state.XmlFragmentNavigator = null; - _state.Position = StatePosition.PropertyElement; - DebugState(); + 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 = 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."); + } } - succ = true; + break; + case StatePosition.Element: case StatePosition.Root: - succ = false; + value = string.Empty; break; default: throw new InvalidOperationException("Invalid position."); } - DebugReturn(succ); - return succ; + DebugReturn("\"{0}\"", value); + return value; } + } - private bool MoveToParentElement() - { - var p = _state.Parent; - if (p != null) - { - _state = p; - DebugState(); - return true; - } + #region State management - return false; - } + // the possible state positions + public enum StatePosition + { + Root, + Element, + Attribute, + PropertyElement, + PropertyText, + PropertyXml, + } - /// - /// Moves the XPathNavigator to the root node that the current node belongs to. - /// - public override void MoveToRoot() - { - DebugEnter("MoveToRoot"); + // gets the state + // for unit tests only + public State InternalState { get; private set; } - while (_state.Parent != null) - _state = _state.Parent; - DebugState(); + // represents the XPathNavigator state + public class State + { + private static readonly int[] NoChildIds = new int[0]; - DebugReturn(); - } + // the current content + private INavigableContent? _content; - /// - /// 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 + // initialize a new state + private State(StatePosition position) { - get - { - DebugEnter("NodeType"); - XPathNodeType type; - - switch (_state.Position) - { - 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."); - } - - DebugReturn("\'{0}\'", type); - return type; - } + Position = position; + FieldIndex = -1; } - /// - /// 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 + // 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) { - get - { - DebugEnter("Value"); - string value; - - switch (_state.Position) - { - case StatePosition.PropertyXml: - value = _state.XmlFragmentNavigator?.Value ?? string.Empty; - break; - case StatePosition.Attribute: - case StatePosition.PropertyText: - case StatePosition.PropertyElement: - if (_state.FieldIndex == -1) - { - value = _state.Content?.Id.ToString(CultureInfo.InvariantCulture) ?? string.Empty; - } - 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."); - } - } - break; - case StatePosition.Element: - case StatePosition.Root: - value = string.Empty; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn("\"{0}\"", value); - return value; - } + Content = content; + Parent = parent; + Depth = parent?.Depth + 1 ?? 0; + Siblings = siblings; + SiblingIndex = siblingIndex; } - #region State management - - // the possible state positions - public enum StatePosition + // initialize a clone state + private State(State other, bool recurse = false) { - 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; } + Position = other.Position; - // initialize a new state - private State(StatePosition position) - { - Position = position; - FieldIndex = -1; - } + _content = other._content; + SiblingIndex = other.SiblingIndex; + Siblings = other.Siblings; + FieldsCount = other.FieldsCount; + FieldIndex = other.FieldIndex; + Depth = other.Depth; - // 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) + if (Position == StatePosition.PropertyXml) { - Content = content; - Parent = parent; - Depth = parent?.Depth + 1 ?? 0; - Siblings = siblings; - SiblingIndex = siblingIndex; + XmlFragmentNavigator = other.XmlFragmentNavigator?.Clone(); } - // initialize a clone state - private State(State other, bool recurse = false) + // 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) { - 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; - } + return; } - public State Clone() + State clone = this; + while (other.Parent != null) { - return new State(this); + clone.Parent = new State(other.Parent, true); + clone = clone.Parent; + other = other.Parent; } + } - // the parent state - public State? Parent { get; private set; } + public StatePosition Position { get; set; } - // the depth - public int Depth { get; } + // the parent state + public State? Parent { get; private set; } - // the current content - private INavigableContent? _content; + // the depth + public int Depth { get; } - // the current content - public INavigableContent? Content + // the current content + public INavigableContent? Content + { + get => _content; + set { - get - { - return _content; - } - set - { - FieldsCount = value?.Type.FieldTypes.Length ?? 0; - _content = value; - } + FieldsCount = value?.Type.FieldTypes.Length ?? 0; + _content = value; } + } - private static readonly int[] NoChildIds = new int[0]; + // the index of the current content within Siblings + public int SiblingIndex { get; set; } - // the current content child ids - public IList GetContentChildIds(int maxDepth) - { - return Depth < maxDepth && _content?.ChildIds != null ? _content.ChildIds : NoChildIds; - } + // the list of content identifiers for all children of the current content's parent + public IList? Siblings { get; } - // the index of the current content within Siblings - public int SiblingIndex { get; set; } + // the number of fields of the current content + // properties include attributes and properties + public int FieldsCount { get; private set; } - // the list of content identifiers for all children of the current content's parent - public IList? Siblings { get; } + // the index of the current field + // index -1 means special attribute "id" + public int FieldIndex { get; set; } - // the number of fields of the current content - // properties include attributes and properties - public int FieldsCount { get; private set; } + // the current field type + // beware, no check on the index + public INavigableFieldType? CurrentFieldType => Content?.Type.FieldTypes[FieldIndex]; - // the index of the current field - // index -1 means special attribute "id" - public int FieldIndex { get; set; } + // gets or sets the xml fragment navigator + public XPathNavigator? XmlFragmentNavigator { get; set; } - // the current field type - // beware, no check on the index - public INavigableFieldType? CurrentFieldType => Content?.Type.FieldTypes[FieldIndex]; + public State Clone() => new State(this); - // gets or sets the xml fragment navigator - public XPathNavigator? XmlFragmentNavigator { get; set; } + // 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) + // 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) { - 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; + return false; } - } - #endregion + 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 364560ebee23..1b710c8db54d 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; + _navigator = navigator; + _rootName = rootName; + } - public RenamedRootNavigator(XPathNavigator navigator, string rootName) - { - _navigator = navigator; - _rootName = rootName; - } + public override string BaseURI => _navigator.BaseURI; - public override string BaseURI => _navigator.BaseURI; + public override bool IsEmptyElement => _navigator.IsEmptyElement; - public override XPathNavigator Clone() + public override string LocalName + { + get { - return new RenamedRootNavigator(_navigator.Clone(), _rootName); - } - - public override bool IsEmptyElement => _navigator.IsEmptyElement; + // local name without prefix + XPathNavigator nav = _navigator.Clone(); + if (nav.MoveToParent() && nav.MoveToParent()) + { + return _navigator.LocalName; + } - public override bool IsSamePosition(XPathNavigator other) - { - return _navigator.IsSamePosition(other); + return _rootName; } + } - public override string LocalName + public override string Name + { + get { - get + // qualified name with prefix + XPathNavigator nav = _navigator.Clone(); + if (nav.MoveToParent() && nav.MoveToParent()) { - // local name without prefix - - var nav = _navigator.Clone(); - if (nav.MoveToParent() && nav.MoveToParent()) - return _navigator.LocalName; - return _rootName; + return _navigator.Name; } - } - public override bool MoveTo(XPathNavigator other) - { - return _navigator.MoveTo(other); + var name = _navigator.Name; + var pos = name.IndexOf(':'); + return pos < 0 ? _rootName : name[..(pos + 1)] + _rootName; } + } - public override bool MoveToFirstAttribute() - { - return _navigator.MoveToFirstAttribute(); - } + public override XmlNameTable NameTable => _navigator.NameTable; - public override bool MoveToFirstChild() - { - return _navigator.MoveToFirstChild(); - } + public override string NamespaceURI => _navigator.NamespaceURI; - public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) - { - return _navigator.MoveToFirstNamespace(namespaceScope); - } + public override XPathNodeType NodeType => _navigator.NodeType; - public override bool MoveToId(string id) - { - return _navigator.MoveToId(id); - } + public override string Prefix => _navigator.Prefix; - public override bool MoveToNext() - { - return _navigator.MoveToNext(); - } + public override string Value => _navigator.Value; - public override bool MoveToNextAttribute() - { - return _navigator.MoveToNextAttribute(); - } + public override XPathNavigator Clone() => new RenamedRootNavigator(_navigator.Clone(), _rootName); - public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) - { - return _navigator.MoveToNextNamespace(namespaceScope); - } + public override bool IsSamePosition(XPathNavigator other) => _navigator.IsSamePosition(other); - public override bool MoveToParent() - { - return _navigator.MoveToParent(); - } + public override bool MoveTo(XPathNavigator other) => _navigator.MoveTo(other); - public override bool MoveToPrevious() - { - return _navigator.MoveToPrevious(); - } + public override bool MoveToFirstAttribute() => _navigator.MoveToFirstAttribute(); - 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 bool MoveToFirstChild() => _navigator.MoveToFirstChild(); - public override XmlNameTable NameTable => _navigator.NameTable; + public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) => + _navigator.MoveToFirstNamespace(namespaceScope); - public override string NamespaceURI => _navigator.NamespaceURI; + public override bool MoveToId(string id) => _navigator.MoveToId(id); - public override XPathNodeType NodeType => _navigator.NodeType; + public override bool MoveToNext() => _navigator.MoveToNext(); - public override string Prefix => _navigator.Prefix; + public override bool MoveToNextAttribute() => _navigator.MoveToNextAttribute(); - public override string Value => _navigator.Value; - } + 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 8006d26da6d2..44cda2c69183 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. + /// + /// 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) + { + 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 (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. /// - 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, XPathExpression 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) + XPathExpression compiled = expression.Clone(); // clone for thread-safety + var context = new DynamicContext(); + foreach (XPathVariable variable in variables) { - if (variables == null || variables.Length == 0 || variables[0] == null) - return navigator.Select(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); + 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 9bfed8e98d17..4c2d2d0f4e9a 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; } + Name = name; + Value = value; + } - /// - /// Gets or sets the value of the variable. - /// - public string Value { get; private set; } + /// + /// Gets or sets the name of the variable. + /// + public string Name { get; } - /// - /// Initializes a new instance of the class with a name and a value. - /// - /// - /// - public XPathVariable(string name, string value) - { - Name = name; - Value = value; - } - } + /// + /// 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 4de056e22393..ad97120c93dc 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) 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; - } + if (xml == null) + { + 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 - - 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! + if (n.Attributes[name] == null) + { + XmlAttribute a = xml.CreateAttribute(name); + a.Value = value; + n.Attributes.Append(a); + } + else + { + n.Attributes[name]!.Value = value; + } + } - //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; + /// + /// 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; + } - 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; } + } + + /// + /// 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) + { + 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 + } - /// - /// 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) + XPathNavigator nav = doc!.CreateNavigator(); + if (nav.MoveToFirstChild()) { - var sortedChildNodes = parentNode.SelectNodes(childNodesXPath)?.Cast() - .OrderBy(orderBy) - .ToArray(); + // 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; + } - // append child nodes to last position, in sort-order - // so all child nodes will go after the property nodes - if (sortedChildNodes is not null) + 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) { - foreach (var node in sortedChildNodes) - parentNode.AppendChild(node); // moves the node to the last position + 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; + } + + // find the first node with a sortOrder > node.sortOrder + var i = 0; + while (i < childNodesAndOrder.Length && childNodesAndOrder[i].Item2 <= nodeSortOrder) + { + i++; + } - /// - /// 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); - var 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; - - // find the first node with a sortOrder > node.sortOrder - var i = 0; - while (i < childNodesAndOrder.Length && childNodesAndOrder[i].Item2 <= nodeSortOrder) - i++; - - // if one was found - if (i < childNodesAndOrder.Length) + // 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) { - // 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; - } + parentNode.InsertBefore(node, childNodesAndOrder[i].Item1); + return true; } - else // i == childNodesAndOrder.Length && childNodesAndOrder.Length > 1 + } + 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) { - // 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); + return true; } - return false; } + 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) + /// + /// 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, + }) { - using (var reader = new XmlTextReader(hostingEnvironment.MapPathContentRoot(filePath)) {WhitespaceHandling = WhitespaceHandling.All}) - { - var xmlDoc = new XmlDocument(); - //Load the file into the XmlDocument - xmlDoc.Load(reader); + var xmlDoc = new XmlDocument(); - return xmlDoc; - } + // 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)); - 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.CreateAttribute(name); - temp.Value = value; - return temp; - } - - /// - /// 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)); - - var temp = xd.CreateNode(XmlNodeType.Element, name, ""); - 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)); - - var child = parent.SelectSingleNode(name); - if (child != null) - { - child.InnerText = value; - return child; - } - return AddTextNode(xd, name, value); - } - - /// - /// 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)); - - var child = parent.SelectSingleNode(name) ?? xd.CreateNode(XmlNodeType.Element, name, ""); - child.InnerXml = value; + /// + /// 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)); + } + + XmlAttribute temp = xd.CreateAttribute(name); + temp.Value = value; + return temp; + } + + /// + /// 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)); - - var temp = xd.CreateNode(XmlNodeType.Element, name, ""); - 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)); - - var child = parent.SelectSingleNode(name); - if (child != null) - { - child.InnerXml = ""; - return child; - } - return AddCDataNode(xd, name, value); + return AddTextNode(xd, name, value); + } + + /// + /// 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)); } - /// - /// Gets the value of a XmlNode - /// - /// The XmlNode. - /// the value as a string - public static string GetNodeValue(XmlNode n) + if (parent == 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(parent)); } - /// - /// 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 (name == null) { - if (string.IsNullOrEmpty(xml)) return false; + throw new ArgumentNullException(nameof(name)); + } - xml = xml.Trim(); - return xml.StartsWith("<") && xml.EndsWith(">") && xml.Contains('/'); + if (string.IsNullOrWhiteSpace(name)) + { + 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 1721de253f11..55a23736ff43 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 29797fc59a87..17c2f418438c 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(); - /// - /// 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) + public XmlNodeListIterator(XPathNodeIterator? iterator) => _iterator = iterator?.Clone(); + + public override int Count { - return new XmlNodeListIterator(iterator); + get + { + if (!Done) + { + ReadToEnd(); + } + + return _nodes.Count; + } } - #endregion Public members + /// + /// Flags that the iterator has been consumed. + /// + private bool Done { get; set; } - #region XmlNodeListIterator + /// + /// Current count of nodes in the iterator (read so far). + /// + private int CurrentPosition => _nodes.Count; - private class XmlNodeListIterator : XmlNodeList - { - readonly XPathNodeIterator? _iterator; - readonly IList _nodes = new List(); + public override IEnumerator GetEnumerator() => new XmlNodeListEnumerator(this); - public XmlNodeListIterator(XPathNodeIterator? iterator) + public override XmlNode? Item(int index) + { + if (index >= _nodes.Count) { - _iterator = iterator?.Clone(); + ReadTo(index); } - public override System.Collections.IEnumerator GetEnumerator() + // Compatible behavior with .NET + if (index >= _nodes.Count || index < 0) { - return new XmlNodeListEnumerator(this); + return null; } - 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]; - } + return _nodes[index]; + } - public override int Count + /// + /// Reads the entire iterator. + /// + private void ReadToEnd() + { + while (_iterator is not null && _iterator.MoveNext()) { - 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 + #region XmlNodeListEnumerator - private class XmlNodeListEnumerator : System.Collections.IEnumerator - { - readonly XmlNodeListIterator _iterator; - int _position = -1; + private class XmlNodeListEnumerator : IEnumerator + { + private readonly XmlNodeListIterator _iterator; + private int _position = -1; - public XmlNodeListEnumerator(XmlNodeListIterator iterator) - { - _iterator = iterator; - } + public XmlNodeListEnumerator(XmlNodeListIterator iterator) => _iterator = iterator; - #region IEnumerator Members + object? IEnumerator.Current => _iterator[_position]; - void System.Collections.IEnumerator.Reset() - { - _position = -1; - } + #region IEnumerator Members + void 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; - } + bool IEnumerator.MoveNext() + { + _position++; + _iterator.ReadTo(_position); - object? System.Collections.IEnumerator.Current + // If we reached the end and our index is still + // bigger, there are no more items. + if (_iterator.Done && _position >= _iterator.CurrentPosition) { - get - { - return _iterator[_position]; - } + return false; } - #endregion + return true; } - #endregion XmlNodeListEnumerator + #endregion } - #endregion XmlNodeListIterator + #endregion XmlNodeListEnumerator } + + #endregion XmlNodeListIterator }