Introduce INavigationService for in-memory navigation data (#16818)

* Tests

* Remove props and use local vars

* Adding preliminary navigation service and content implementation

* Adding preliminary unit tests

* Change from async methods

* Refactor GetParentKey to TryGetParentKey

* Refactor GetChildrenKeys to TryGetChildrenKeys

* Refactor GetDescendantsKeys to TryGetDescendantsKeys

* Refactor GetAncestorsKeys to TryGetAncestorsKeys

* Refactor GetSiblingsKeys to TryGetSiblingsKeys

* Refactor TryGetChildrenKeys

* Initial integration tests

* Use ContentEditingService instead of ContentService

* Remove INavigationService.Copy implementation and unit tests

* Rename var

* Adding clarification

* Initial ContentNavigationRepository

* Initial NavigationFactory

* Remove filtering from factory

* NavigationRepository and implementation

* InitializationService responsible for seeding the in-memory structure

* Register repository and service

* Adding NavigationDto and NavigationNode

* Adding INavigationService dependency and Enlist updating navigation structure actions

* Documentation

* Adding tests for removing descendants as well

* Changed to ConcurrentDictionary

* Remove keys comments for tests

* Adding documentation

* Forgotten ConcurrentDictionary change

* Isolating the operations on the model

* Splitting the INavigationService to separate the querying from the managing functionality

* Introducing specific navigation services for document, document recycle bin, media and media recycle bin

* Making ContentNavigationService into a base as the functionality will be shared between the document, document recycle bin, media and media recycle bin services

* Adding the implementations of document, document recycle bin, media and media recycle bin navigation services

* Fixing comments

* Initializing all 4 collections

* Adapting the navigation unit tests to the base now

* Adapting integration tests to specific navigation service

* Adding test for rebuilding the structure

* Adding implementation for Adding and Getting a node - needed for moving to and restoring from the recycle bin + tests

* Updating the document navigation structure from the ContentService

* Fix typo

* Adding trashed items implementation in base - currently managing 2 structures

* Removing no longer relevant GetNavigationNode and AddNavigationNode

* Fix removing parent when child is removed supporting methods

* Added restoring functionality

* Adding Bin functionality to DocumentNavigationService

* Removing Move signature from IDocumentNavigationService

* Adding RecycleBin query and management services

* Re-adding Move and removing GetNavigationNode and AddNavigationNode signatures from interface

* Rebuilding bin structure using _documentNavigationService, instead of _documentRecycleBinNavigationService

* Fixing test name

* Adding more tests for remove

* Adding tests for restore and removing ones for GetNavigationNode and AddNavigationNode

* Remove comments

* Removing document and media RecycleBinNavigationService and their interfaces

* Adding media rebuild bin

* Fixing initialization with correct interfaces

* Removing RecycleBinNavigationServices' registration

* Remove IDocumentRecycleBinNavigationService dependency

* Updating in-memory nav structure when content updates happen

* Adding the rest of the integration tests

* Clean up IMediaNavigationService

* Fix comments

* Remove CustomTestSetup in integration tests as the structure is updated when content updates happen

* Adding and fixing comments

* Making RebuildBinAsync abstract as well

* Adding DocumentNavigationServiceTestsBase

* Splitting DocumentNavigationServiceTests into partial test classes

* Cleaning up DocumentNavigationServiceTests since tests have been moved to specific partial classes

* Reuse a method for creating content in tests

* Change type in test base

* Adding navigation structure updates in media service

* Adding MediaNavigationServiceTestsBase

* Adding integration tests for media nav str

* Remove services as we will have more concrete ones

* Add document and media IXNavigationQueryService and IXNavigationManagementService

* Inject ManagementService in ContentService.cs and MediaService.cs

* Change implementation to implement the new services + registration

* Make classes sealed

* Inject correct services in InitializationService

* Using the right services in integration tests

* Adding comments

* Removing bin interfaces from main navigation ones

* Rename Remove to MoveToBin

* V14 QA added block list editor tests (#16862)

* Added tests for blocklistEditor

* Added more tets

* Removed faker

* Added blockTest

* Updates

* Added tests

* Removed dependencies

* Fixes

* Clean up

* Fixed naming

* Cleaned up

* Bumped version

* Added missing semicolons

* Added tags

* Only runs the new tests

* Updates

* Bumped version

* Fixed tests

* Cleaned up

* Updated version

* Fixes, not done

* Fixed tests

* Bumped helpers

* Bumped helpers

* Fixed conflict

* Fixed comment

* Reverted to run smokeTests

* Updated helpers

* improve missingProperties data returned for missing propertie values (#16910)

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* update backoffice submodule

* Rename initialization service to initialization hosted service

* Refactor repository to return a collection

* Add interface for the NavigationDto

* Add constants to bind property names between DTOs

* Move factory and fix input type

* Use constants for column names

* Use factory from base

---------

Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
Co-authored-by: Sven Geusens <sge@umbraco.dk>
Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
This commit is contained in:
Elitsa Marinovska
2024-09-04 11:18:08 +02:00
committed by GitHub
parent eff520c7eb
commit 5a7d563b8a
54 changed files with 3669 additions and 770 deletions

View File

@@ -39,6 +39,7 @@ using Umbraco.Cms.Core.Preview;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services.FileSystem;
using Umbraco.Cms.Core.Services.ImportExport;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Core.Services.Querying.RecycleBin;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Core.Telemetry;
@@ -338,8 +339,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
factory.GetRequiredService<ICoreScopeProvider>(),
factory.GetRequiredService<ILoggerFactory>(),
factory.GetRequiredService<IEventMessagesFactory>(),
factory.GetRequiredService<IExternalLoginWithKeyRepository>()
));
factory.GetRequiredService<IExternalLoginWithKeyRepository>()));
Services.AddUnique<ILogViewerService, LogViewerService>();
Services.AddUnique<IExternalLoginWithKeyService>(factory => factory.GetRequiredService<ExternalLoginService>());
Services.AddUnique<ILocalizedTextService>(factory => new LocalizedTextService(
@@ -352,6 +352,12 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddSingleton<CompiledPackageXmlParser>();
Services.AddUnique<IPreviewTokenGenerator, NoopPreviewTokenGenerator>();
Services.AddUnique<IPreviewService, PreviewService>();
Services.AddUnique<DocumentNavigationService, DocumentNavigationService>();
Services.AddUnique<IDocumentNavigationQueryService>(x => x.GetRequiredService<DocumentNavigationService>());
Services.AddUnique<IDocumentNavigationManagementService>(x => x.GetRequiredService<DocumentNavigationService>());
Services.AddUnique<MediaNavigationService, MediaNavigationService>();
Services.AddUnique<IMediaNavigationQueryService>(x => x.GetRequiredService<MediaNavigationService>());
Services.AddUnique<IMediaNavigationManagementService>(x => x.GetRequiredService<MediaNavigationService>());
// Register a noop IHtmlSanitizer & IMarkdownSanitizer to be replaced
Services.AddUnique<IHtmlSanitizer, NoopHtmlSanitizer>();

View File

@@ -0,0 +1,45 @@
using System.Collections.Concurrent;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Navigation;
namespace Umbraco.Cms.Core.Factories;
internal static class NavigationFactory
{
/// <summary>
/// Builds a dictionary of NavigationNode objects from a given dataset.
/// </summary>
/// <param name="entities">The <see cref="INavigationModel" /> objects used to build the navigation nodes dictionary.</param>
/// <returns>A dictionary of <see cref="NavigationNode" /> objects with key corresponding to their unique Guid.</returns>
public static ConcurrentDictionary<Guid, NavigationNode> BuildNavigationDictionary(IEnumerable<INavigationModel> entities)
{
var nodesStructure = new ConcurrentDictionary<Guid, NavigationNode>();
var entityList = entities.ToList();
var idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key);
foreach (INavigationModel entity in entityList)
{
var node = new NavigationNode(entity.Key);
nodesStructure[entity.Key] = node;
// We don't set the parent for items under root, it will stay null
if (entity.ParentId == -1)
{
continue;
}
if (idToKeyMap.TryGetValue(entity.ParentId, out Guid parentKey) is false)
{
continue;
}
// If the parent node exists in the nodesStructure, add the node to the parent's children (parent is set as well)
if (nodesStructure.TryGetValue(parentKey, out NavigationNode? parentNode))
{
parentNode.AddChild(node);
}
}
return nodesStructure;
}
}

View File

@@ -0,0 +1,24 @@
namespace Umbraco.Cms.Core.Models;
public interface INavigationModel
{
/// <summary>
/// Gets or sets the integer identifier of the entity.
/// </summary>
int Id { get; set; }
/// <summary>
/// Gets or sets the Guid unique identifier of the entity.
/// </summary>
Guid Key { get; set; }
/// <summary>
/// Gets or sets the integer identifier of the parent entity.
/// </summary>
int ParentId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this entity is in the recycle bin.
/// </summary>
bool Trashed { get; set; }
}

View File

@@ -0,0 +1,30 @@
namespace Umbraco.Cms.Core.Models.Navigation;
public sealed class NavigationNode
{
private List<NavigationNode> _children;
public Guid Key { get; private set; }
public NavigationNode? Parent { get; private set; }
public IEnumerable<NavigationNode> Children => _children.AsEnumerable();
public NavigationNode(Guid key)
{
Key = key;
_children = new List<NavigationNode>();
}
public void AddChild(NavigationNode child)
{
child.Parent = this;
_children.Add(child);
}
public void RemoveChild(NavigationNode child)
{
_children.Remove(child);
child.Parent = null;
}
}

View File

@@ -0,0 +1,20 @@
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Persistence.Repositories;
public interface INavigationRepository
{
/// <summary>
/// Retrieves a collection of content nodes as navigation models based on the object type key.
/// </summary>
/// <param name="objectTypeKey">The unique identifier for the object type.</param>
/// <returns>A collection of navigation models.</returns>
IEnumerable<INavigationModel> GetContentNodesByObjectType(Guid objectTypeKey);
/// <summary>
/// Retrieves a collection of trashed content nodes as navigation models based on the object type key.
/// </summary>
/// <param name="objectTypeKey">The unique identifier for the object type.</param>
/// <returns>A collection of navigation models.</returns>
IEnumerable<INavigationModel> GetTrashedContentNodesByObjectType(Guid objectTypeKey);
}

View File

@@ -12,6 +12,7 @@ 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.Cms.Core.Services.Navigation;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
@@ -33,25 +34,27 @@ public class ContentService : RepositoryService, IContentService
private readonly IShortStringHelper _shortStringHelper;
private readonly ICultureImpactFactory _cultureImpactFactory;
private readonly IUserIdKeyResolver _userIdKeyResolver;
private readonly IDocumentNavigationManagementService _documentNavigationManagementService;
private IQuery<IContent>? _queryNotTrashed;
#region Constructors
public ContentService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IDocumentRepository documentRepository,
IEntityRepository entityRepository,
IAuditRepository auditRepository,
IContentTypeRepository contentTypeRepository,
IDocumentBlueprintRepository documentBlueprintRepository,
ILanguageRepository languageRepository,
Lazy<IPropertyValidationService> propertyValidationService,
IShortStringHelper shortStringHelper,
ICultureImpactFactory cultureImpactFactory,
IUserIdKeyResolver userIdKeyResolver)
: base(provider, loggerFactory, eventMessagesFactory)
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IDocumentRepository documentRepository,
IEntityRepository entityRepository,
IAuditRepository auditRepository,
IContentTypeRepository contentTypeRepository,
IDocumentBlueprintRepository documentBlueprintRepository,
ILanguageRepository languageRepository,
Lazy<IPropertyValidationService> propertyValidationService,
IShortStringHelper shortStringHelper,
ICultureImpactFactory cultureImpactFactory,
IUserIdKeyResolver userIdKeyResolver,
IDocumentNavigationManagementService documentNavigationManagementService)
: base(provider, loggerFactory, eventMessagesFactory)
{
_documentRepository = documentRepository;
_entityRepository = entityRepository;
@@ -63,9 +66,43 @@ public class ContentService : RepositoryService, IContentService
_shortStringHelper = shortStringHelper;
_cultureImpactFactory = cultureImpactFactory;
_userIdKeyResolver = userIdKeyResolver;
_documentNavigationManagementService = documentNavigationManagementService;
_logger = loggerFactory.CreateLogger<ContentService>();
}
[Obsolete("Use non-obsolete constructor. Scheduled for removal in V16.")]
public ContentService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IDocumentRepository documentRepository,
IEntityRepository entityRepository,
IAuditRepository auditRepository,
IContentTypeRepository contentTypeRepository,
IDocumentBlueprintRepository documentBlueprintRepository,
ILanguageRepository languageRepository,
Lazy<IPropertyValidationService> propertyValidationService,
IShortStringHelper shortStringHelper,
ICultureImpactFactory cultureImpactFactory,
IUserIdKeyResolver userIdKeyResolver)
: this(
provider,
loggerFactory,
eventMessagesFactory,
documentRepository,
entityRepository,
auditRepository,
contentTypeRepository,
documentBlueprintRepository,
languageRepository,
propertyValidationService,
shortStringHelper,
cultureImpactFactory,
userIdKeyResolver,
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationManagementService>())
{
}
[Obsolete("Use constructor that takes IUserIdKeyResolver as a parameter, scheduled for removal in V15")]
public ContentService(
ICoreScopeProvider provider,
@@ -93,7 +130,8 @@ public class ContentService : RepositoryService, IContentService
propertyValidationService,
shortStringHelper,
cultureImpactFactory,
StaticServiceProvider.Instance.GetRequiredService<IUserIdKeyResolver>())
StaticServiceProvider.Instance.GetRequiredService<IUserIdKeyResolver>(),
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationManagementService>())
{
}
@@ -1034,6 +1072,11 @@ public class ContentService : RepositoryService, IContentService
// have always changed if it's been saved in the back office but that's not really fail safe.
_documentRepository.Save(content);
// Updates in-memory navigation structure - we only handle new items, other updates are not a concern
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.ContentService.Save-with-contentSchedule",
() => _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key));
if (contentSchedule != null)
{
_documentRepository.PersistContentSchedule(content, contentSchedule);
@@ -1097,6 +1140,11 @@ public class ContentService : RepositoryService, IContentService
content.WriterId = userId;
_documentRepository.Save(content);
// Updates in-memory navigation structure - we only handle new items, other updates are not a concern
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.ContentService.Save",
() => _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key));
}
scope.Notifications.Publish(
@@ -2288,6 +2336,26 @@ public class ContentService : RepositoryService, IContentService
}
DoDelete(content);
if (content.Trashed)
{
// Updates in-memory navigation structure for recycle bin items
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.ContentService.DeleteLocked-trashed",
() => _documentNavigationManagementService.RemoveFromBin(content.Key));
}
else
{
// Updates in-memory navigation structure for both documents and recycle bin items
// as the item needs to be deleted whether it is in the recycle bin or not
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.ContentService.DeleteLocked",
() =>
{
_documentNavigationManagementService.MoveToBin(content.Key);
_documentNavigationManagementService.RemoveFromBin(content.Key);
});
}
}
// TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way
@@ -2512,6 +2580,8 @@ public class ContentService : RepositoryService, IContentService
// 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)
{
// Needed to update the in-memory navigation structure
var cameFromRecycleBin = content.ParentId == Constants.System.RecycleBinContent;
content.WriterId = userId;
content.ParentId = parentId;
@@ -2560,6 +2630,33 @@ public class ContentService : RepositoryService, IContentService
}
}
while (total > pageSize);
if (parentId == Constants.System.RecycleBinContent)
{
// Updates in-memory navigation structure for both document items and recycle bin items
// as we are moving to recycle bin
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked-to-recycle-bin",
() => _documentNavigationManagementService.MoveToBin(content.Key));
}
else
{
if (cameFromRecycleBin)
{
// Updates in-memory navigation structure for both document items and recycle bin items
// as we are restoring from recycle bin
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked-restore",
() => _documentNavigationManagementService.RestoreFromBin(content.Key, parent?.Key));
}
else
{
// Updates in-memory navigation structure
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked",
() => _documentNavigationManagementService.Move(content.Key, parent?.Key));
}
}
}
private void PerformMoveContentLocked(IContent content, int userId, bool? trash)
@@ -2663,6 +2760,9 @@ public class ContentService : RepositoryService, IContentService
{
EventMessages eventMessages = EventMessagesFactory.Get();
// keep track of updates (copied item key and parent key) for the in-memory navigation structure
var navigationUpdates = new List<Tuple<Guid, Guid?>>();
IContent copy = content.DeepCloneWithResetIdentities();
copy.ParentId = parentId;
@@ -2699,6 +2799,9 @@ public class ContentService : RepositoryService, IContentService
// save and flush because we need the ID for the recursive Copying events
_documentRepository.Save(copy);
// store navigation update information for copied item
navigationUpdates.Add(Tuple.Create(copy.Key, GetParent(copy)?.Key));
// add permissions
if (currentPermissions.Count > 0)
{
@@ -2750,12 +2853,29 @@ public class ContentService : RepositoryService, IContentService
// save and flush (see above)
_documentRepository.Save(descendantCopy);
// store navigation update information for descendants
navigationUpdates.Add(Tuple.Create(descendantCopy.Key, GetParent(descendantCopy)?.Key));
copies.Add(Tuple.Create(descendant, descendantCopy));
idmap[descendant.Id] = descendantCopy.Id;
}
}
}
if (navigationUpdates.Count > 0)
{
// Updates in-memory navigation structure
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.ContentService.Copy",
() =>
{
foreach (Tuple<Guid, Guid?> update in navigationUpdates)
{
_documentNavigationManagementService.Add(update.Item1, update.Item2);
}
});
}
// 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
@@ -3697,4 +3817,29 @@ public class ContentService : RepositoryService, IContentService
DeleteBlueprintsOfTypes(new[] { contentTypeId }, userId);
#endregion
/// <summary>
/// Enlists an action in the current scope context to update the in-memory navigation structure
/// when the scope completes successfully.
/// </summary>
/// <param name="enlistingActionKey">The unique key identifying the action to be enlisted.</param>
/// <param name="updateNavigation">The action to be performed for updating the in-memory navigation structure.</param>
/// <exception cref="NullReferenceException">Thrown when the scope context is null and therefore cannot be used.</exception>
private void UpdateInMemoryNavigationStructure(string enlistingActionKey, Action updateNavigation)
{
IScopeContext? scopeContext = ScopeProvider.Context;
if (scopeContext is null)
{
throw new NullReferenceException($"The {nameof(scopeContext)} is null and cannot be used.");
}
scopeContext.Enlist(enlistingActionKey, completed =>
{
if (completed)
{
updateNavigation();
}
});
}
}

View File

@@ -12,6 +12,7 @@ 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.Cms.Core.Services.Navigation;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
@@ -28,6 +29,7 @@ namespace Umbraco.Cms.Core.Services
private readonly IEntityRepository _entityRepository;
private readonly IShortStringHelper _shortStringHelper;
private readonly IUserIdKeyResolver _userIdKeyResolver;
private readonly IMediaNavigationManagementService _mediaNavigationManagementService;
private readonly MediaFileManager _mediaFileManager;
@@ -43,7 +45,8 @@ namespace Umbraco.Cms.Core.Services
IMediaTypeRepository mediaTypeRepository,
IEntityRepository entityRepository,
IShortStringHelper shortStringHelper,
IUserIdKeyResolver userIdKeyResolver)
IUserIdKeyResolver userIdKeyResolver,
IMediaNavigationManagementService mediaNavigationManagementService)
: base(provider, loggerFactory, eventMessagesFactory)
{
_mediaFileManager = mediaFileManager;
@@ -53,6 +56,34 @@ namespace Umbraco.Cms.Core.Services
_entityRepository = entityRepository;
_shortStringHelper = shortStringHelper;
_userIdKeyResolver = userIdKeyResolver;
_mediaNavigationManagementService = mediaNavigationManagementService;
}
[Obsolete("Use non-obsolete constructor. Scheduled for removal in V16.")]
public MediaService(
ICoreScopeProvider provider,
MediaFileManager mediaFileManager,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IMediaRepository mediaRepository,
IAuditRepository auditRepository,
IMediaTypeRepository mediaTypeRepository,
IEntityRepository entityRepository,
IShortStringHelper shortStringHelper,
IUserIdKeyResolver userIdKeyResolver)
: this(
provider,
mediaFileManager,
loggerFactory,
eventMessagesFactory,
mediaRepository,
auditRepository,
mediaTypeRepository,
entityRepository,
shortStringHelper,
userIdKeyResolver,
StaticServiceProvider.Instance.GetRequiredService<IMediaNavigationManagementService>())
{
}
[Obsolete("Use constructor that takes IUserIdKeyResolver as a parameter, scheduled for removal in V15")]
@@ -76,8 +107,8 @@ namespace Umbraco.Cms.Core.Services
mediaTypeRepository,
entityRepository,
shortStringHelper,
StaticServiceProvider.Instance.GetRequiredService<IUserIdKeyResolver>()
)
StaticServiceProvider.Instance.GetRequiredService<IUserIdKeyResolver>(),
StaticServiceProvider.Instance.GetRequiredService<IMediaNavigationManagementService>())
{
}
@@ -769,6 +800,12 @@ namespace Umbraco.Cms.Core.Services
media.WriterId = userId;
_mediaRepository.Save(media);
// Updates in-memory navigation structure - we only handle new items, other updates are not a concern
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.MediaService.Save",
() => _mediaNavigationManagementService.Add(media.Key, GetParent(media)?.Key));
scope.Notifications.Publish(new MediaSavedNotification(media, eventMessages).WithStateFrom(savingNotification));
// TODO: See note about suppressing events in content service
scope.Notifications.Publish(new MediaTreeChangeNotification(media, TreeChangeTypes.RefreshNode, eventMessages));
@@ -810,6 +847,11 @@ namespace Umbraco.Cms.Core.Services
}
_mediaRepository.Save(media);
// Updates in-memory navigation structure - we only handle new items, other updates are not a concern
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.ContentService.Save-collection",
() => _mediaNavigationManagementService.Add(media.Key, GetParent(media)?.Key));
}
scope.Notifications.Publish(new MediaSavedNotification(mediasA, messages).WithStateFrom(savingNotification));
@@ -881,6 +923,26 @@ namespace Umbraco.Cms.Core.Services
}
DoDelete(media);
if (media.Trashed)
{
// Updates in-memory navigation structure for recycle bin items
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.MediaService.DeleteLocked-trashed",
() => _mediaNavigationManagementService.RemoveFromBin(media.Key));
}
else
{
// Updates in-memory navigation structure for both media and recycle bin items
// as the item needs to be deleted whether it is in the recycle bin or not
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.MediaService.DeleteLocked",
() =>
{
_mediaNavigationManagementService.MoveToBin(media.Key);
_mediaNavigationManagementService.RemoveFromBin(media.Key);
});
}
}
//TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way
@@ -1069,6 +1131,8 @@ namespace Umbraco.Cms.Core.Services
// trash indicates whether we are trashing, un-trashing, or not changing anything
private void PerformMoveLocked(IMedia media, int parentId, IMedia? parent, int userId, ICollection<(IMedia, string)> moves, bool? trash)
{
// Needed to update the in-memory navigation structure
var cameFromRecycleBin = media.ParentId == Constants.System.RecycleBinMedia;
media.ParentId = parentId;
// get the level delta (old pos to new pos)
@@ -1114,6 +1178,32 @@ namespace Umbraco.Cms.Core.Services
}
while (total > pageSize);
if (parentId == Constants.System.RecycleBinMedia)
{
// Updates in-memory navigation structure for both media items and recycle bin items
// as we are moving to recycle bin
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked-to-recycle-bin",
() => _mediaNavigationManagementService.MoveToBin(media.Key));
}
else
{
if (cameFromRecycleBin)
{
// Updates in-memory navigation structure for both media items and recycle bin items
// as we are restoring from recycle bin
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked-restore",
() => _mediaNavigationManagementService.RestoreFromBin(media.Key, parent?.Key));
}
else
{
// Updates in-memory navigation structure
UpdateInMemoryNavigationStructure(
"Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked",
() => _mediaNavigationManagementService.Move(media.Key, parent?.Key));
}
}
}
private void PerformMoveMediaLocked(IMedia media, bool? trash)
@@ -1421,6 +1511,29 @@ namespace Umbraco.Cms.Core.Services
#endregion
/// <summary>
/// Enlists an action in the current scope context to update the in-memory navigation structure
/// when the scope completes successfully.
/// </summary>
/// <param name="enlistingActionKey">The unique key identifying the action to be enlisted.</param>
/// <param name="updateNavigation">The action to be performed for updating the in-memory navigation structure.</param>
/// <exception cref="NullReferenceException">Thrown when the scope context is null and therefore cannot be used.</exception>
private void UpdateInMemoryNavigationStructure(string enlistingActionKey, Action updateNavigation)
{
IScopeContext? scopeContext = ScopeProvider.Context;
if (scopeContext is null)
{
throw new NullReferenceException($"The {nameof(scopeContext)} is null and cannot be used.");
}
scopeContext.Enlist(enlistingActionKey, completed =>
{
if (completed)
{
updateNavigation();
}
});
}
}
}

View File

@@ -0,0 +1,344 @@
using System.Collections.Concurrent;
using Umbraco.Cms.Core.Factories;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Navigation;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
namespace Umbraco.Cms.Core.Services.Navigation;
internal abstract class ContentNavigationServiceBase
{
private readonly ICoreScopeProvider _coreScopeProvider;
private readonly INavigationRepository _navigationRepository;
private ConcurrentDictionary<Guid, NavigationNode> _navigationStructure = new();
private ConcurrentDictionary<Guid, NavigationNode> _recycleBinNavigationStructure = new();
protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository)
{
_coreScopeProvider = coreScopeProvider;
_navigationRepository = navigationRepository;
}
/// <summary>
/// Rebuilds the entire main navigation structure. Implementations should define how the structure is rebuilt.
/// </summary>
public abstract Task RebuildAsync();
/// <summary>
/// Rebuilds the recycle bin navigation structure. Implementations should define how the bin structure is rebuilt.
/// </summary>
public abstract Task RebuildBinAsync();
public bool TryGetParentKey(Guid childKey, out Guid? parentKey)
=> TryGetParentKeyFromStructure(_navigationStructure, childKey, out parentKey);
public bool TryGetChildrenKeys(Guid parentKey, out IEnumerable<Guid> childrenKeys)
=> TryGetChildrenKeysFromStructure(_navigationStructure, parentKey, out childrenKeys);
public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable<Guid> descendantsKeys)
=> TryGetDescendantsKeysFromStructure(_navigationStructure, parentKey, out descendantsKeys);
public bool TryGetAncestorsKeys(Guid childKey, out IEnumerable<Guid> ancestorsKeys)
=> TryGetAncestorsKeysFromStructure(_navigationStructure, childKey, out ancestorsKeys);
public bool TryGetSiblingsKeys(Guid key, out IEnumerable<Guid> siblingsKeys)
=> TryGetSiblingsKeysFromStructure(_navigationStructure, key, out siblingsKeys);
public bool TryGetParentKeyInBin(Guid childKey, out Guid? parentKey)
=> TryGetParentKeyFromStructure(_recycleBinNavigationStructure, childKey, out parentKey);
public bool TryGetChildrenKeysInBin(Guid parentKey, out IEnumerable<Guid> childrenKeys)
=> TryGetChildrenKeysFromStructure(_recycleBinNavigationStructure, parentKey, out childrenKeys);
public bool TryGetDescendantsKeysInBin(Guid parentKey, out IEnumerable<Guid> descendantsKeys)
=> TryGetDescendantsKeysFromStructure(_recycleBinNavigationStructure, parentKey, out descendantsKeys);
public bool TryGetAncestorsKeysInBin(Guid childKey, out IEnumerable<Guid> ancestorsKeys)
=> TryGetAncestorsKeysFromStructure(_recycleBinNavigationStructure, childKey, out ancestorsKeys);
public bool TryGetSiblingsKeysInBin(Guid key, out IEnumerable<Guid> siblingsKeys)
=> TryGetSiblingsKeysFromStructure(_recycleBinNavigationStructure, key, out siblingsKeys);
public bool MoveToBin(Guid key)
{
if (TryRemoveNodeFromParentInStructure(_navigationStructure, key, out NavigationNode? nodeToRemove) is false || nodeToRemove is null)
{
return false; // Node doesn't exist
}
// Recursively remove all descendants and add them to recycle bin
AddDescendantsToRecycleBinRecursively(nodeToRemove);
return _recycleBinNavigationStructure.TryAdd(nodeToRemove.Key, nodeToRemove) &&
_navigationStructure.TryRemove(key, out _);
}
public bool Add(Guid key, Guid? parentKey = null)
{
NavigationNode? parentNode = null;
if (parentKey.HasValue)
{
if (_navigationStructure.TryGetValue(parentKey.Value, out parentNode) is false)
{
return false; // Parent node doesn't exist
}
}
var newNode = new NavigationNode(key);
if (_navigationStructure.TryAdd(key, newNode) is false)
{
return false; // Node with this key already exists
}
parentNode?.AddChild(newNode);
return true;
}
public bool Move(Guid key, Guid? targetParentKey = null)
{
if (_navigationStructure.TryGetValue(key, out NavigationNode? nodeToMove) is false)
{
return false; // Node doesn't exist
}
if (key == targetParentKey)
{
return false; // Cannot move a node to itself
}
NavigationNode? targetParentNode = null;
if (targetParentKey.HasValue && _navigationStructure.TryGetValue(targetParentKey.Value, out targetParentNode) is false)
{
return false; // Target parent doesn't exist
}
// Remove the node from its current parent's children list
if (nodeToMove.Parent is not null && _navigationStructure.TryGetValue(nodeToMove.Parent.Key, out var currentParentNode))
{
currentParentNode.RemoveChild(nodeToMove);
}
// Set the new parent for the node (if parent node is null - the node is moved to root)
targetParentNode?.AddChild(nodeToMove);
return true;
}
public bool RemoveFromBin(Guid key)
{
if (TryRemoveNodeFromParentInStructure(_recycleBinNavigationStructure, key, out NavigationNode? nodeToRemove) is false || nodeToRemove is null)
{
return false; // Node doesn't exist
}
RemoveDescendantsRecursively(nodeToRemove);
return _recycleBinNavigationStructure.TryRemove(key, out _);
}
public bool RestoreFromBin(Guid key, Guid? targetParentKey = null)
{
if (_recycleBinNavigationStructure.TryGetValue(key, out NavigationNode? nodeToRestore) is false)
{
return false; // Node doesn't exist
}
// If a target parent is specified, try to find it in the main structure
NavigationNode? targetParentNode = null;
if (targetParentKey.HasValue && _navigationStructure.TryGetValue(targetParentKey.Value, out targetParentNode) is false)
{
return false; // Target parent doesn't exist
}
// Set the new parent for the node (if parent node is null - the node is moved to root)
targetParentNode?.AddChild(nodeToRestore);
// Restore the node and its descendants from the recycle bin to the main structure
RestoreNodeAndDescendantsRecursively(nodeToRestore);
return _navigationStructure.TryAdd(nodeToRestore.Key, nodeToRestore) &&
_recycleBinNavigationStructure.TryRemove(key, out _);
}
/// <summary>
/// Rebuilds the navigation structure based on the specified object type key and whether the items are trashed.
/// Only relevant for items in the content and media trees (which have readLock values of -333 or -334).
/// </summary>
/// <param name="readLock">The read lock value, should be -333 or -334 for content and media trees.</param>
/// <param name="objectTypeKey">The key of the object type to rebuild.</param>
/// <param name="trashed">Indicates whether the items are in the recycle bin.</param>
protected async Task HandleRebuildAsync(int readLock, Guid objectTypeKey, bool trashed)
{
// This is only relevant for items in the content and media trees
if (readLock != Constants.Locks.ContentTree && readLock != Constants.Locks.MediaTree)
{
return;
}
using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true);
scope.ReadLock(readLock);
IEnumerable<INavigationModel> navigationModels = trashed ?
_navigationRepository.GetTrashedContentNodesByObjectType(objectTypeKey) :
_navigationRepository.GetContentNodesByObjectType(objectTypeKey);
_navigationStructure = NavigationFactory.BuildNavigationDictionary(navigationModels);
}
private bool TryGetParentKeyFromStructure(ConcurrentDictionary<Guid, NavigationNode> structure, Guid childKey, out Guid? parentKey)
{
if (structure.TryGetValue(childKey, out NavigationNode? childNode))
{
parentKey = childNode.Parent?.Key;
return true;
}
// Child doesn't exist
parentKey = null;
return false;
}
private bool TryGetChildrenKeysFromStructure(ConcurrentDictionary<Guid, NavigationNode> structure, Guid parentKey, out IEnumerable<Guid> childrenKeys)
{
if (structure.TryGetValue(parentKey, out NavigationNode? parentNode) is false)
{
// Parent doesn't exist
childrenKeys = [];
return false;
}
childrenKeys = parentNode.Children.Select(child => child.Key);
return true;
}
private bool TryGetDescendantsKeysFromStructure(ConcurrentDictionary<Guid, NavigationNode> structure, Guid parentKey, out IEnumerable<Guid> descendantsKeys)
{
var descendants = new List<Guid>();
if (structure.TryGetValue(parentKey, out NavigationNode? parentNode) is false)
{
// Parent doesn't exist
descendantsKeys = [];
return false;
}
GetDescendantsRecursively(parentNode, descendants);
descendantsKeys = descendants;
return true;
}
private bool TryGetAncestorsKeysFromStructure(ConcurrentDictionary<Guid, NavigationNode> structure, Guid childKey, out IEnumerable<Guid> ancestorsKeys)
{
var ancestors = new List<Guid>();
if (structure.TryGetValue(childKey, out NavigationNode? childNode) is false)
{
// Child doesn't exist
ancestorsKeys = [];
return false;
}
while (childNode?.Parent is not null)
{
ancestors.Add(childNode.Parent.Key);
childNode = childNode.Parent;
}
ancestorsKeys = ancestors;
return true;
}
private bool TryGetSiblingsKeysFromStructure(ConcurrentDictionary<Guid, NavigationNode> structure, Guid key, out IEnumerable<Guid> siblingsKeys)
{
siblingsKeys = [];
if (structure.TryGetValue(key, out NavigationNode? node) is false)
{
return false; // Node doesn't exist
}
if (node.Parent is null)
{
// To find siblings of a node at root level, we need to iterate over all items and add those with null Parent
siblingsKeys = structure
.Where(kv => kv.Value.Parent is null && kv.Key != key)
.Select(kv => kv.Key)
.ToList();
return true;
}
if (TryGetChildrenKeys(node.Parent.Key, out IEnumerable<Guid> childrenKeys) is false)
{
return false; // Couldn't retrieve children keys
}
// Filter out the node itself to get its siblings
siblingsKeys = childrenKeys.Where(childKey => childKey != key).ToList();
return true;
}
private void GetDescendantsRecursively(NavigationNode node, List<Guid> descendants)
{
foreach (NavigationNode child in node.Children)
{
descendants.Add(child.Key);
GetDescendantsRecursively(child, descendants);
}
}
private bool TryRemoveNodeFromParentInStructure(ConcurrentDictionary<Guid, NavigationNode> structure, Guid key, out NavigationNode? nodeToRemove)
{
if (structure.TryGetValue(key, out nodeToRemove) is false)
{
return false; // Node doesn't exist
}
// Remove the node from its parent's children list
if (nodeToRemove.Parent is not null && structure.TryGetValue(nodeToRemove.Parent.Key, out NavigationNode? parentNode))
{
parentNode.RemoveChild(nodeToRemove);
}
return true;
}
private void AddDescendantsToRecycleBinRecursively(NavigationNode node)
{
foreach (NavigationNode child in node.Children)
{
AddDescendantsToRecycleBinRecursively(child);
// Only remove the child from the main structure if it was successfully added to the recycle bin
if (_recycleBinNavigationStructure.TryAdd(child.Key, child))
{
_navigationStructure.TryRemove(child.Key, out _);
}
}
}
private void RemoveDescendantsRecursively(NavigationNode node)
{
foreach (NavigationNode child in node.Children)
{
RemoveDescendantsRecursively(child);
_recycleBinNavigationStructure.TryRemove(child.Key, out _);
}
}
private void RestoreNodeAndDescendantsRecursively(NavigationNode node)
{
foreach (NavigationNode child in node.Children)
{
RestoreNodeAndDescendantsRecursively(child);
// Only remove the child from the recycle bin structure if it was successfully added to the main one
if (_navigationStructure.TryAdd(child.Key, child))
{
_recycleBinNavigationStructure.TryRemove(child.Key, out _);
}
}
}
}

View File

@@ -0,0 +1,18 @@
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
namespace Umbraco.Cms.Core.Services.Navigation;
internal sealed class DocumentNavigationService : ContentNavigationServiceBase, IDocumentNavigationQueryService, IDocumentNavigationManagementService
{
public DocumentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository)
: base(coreScopeProvider, navigationRepository)
{
}
public override async Task RebuildAsync()
=> await HandleRebuildAsync(Constants.Locks.ContentTree, Constants.ObjectTypes.Document, false);
public override async Task RebuildBinAsync()
=> await HandleRebuildAsync(Constants.Locks.ContentTree, Constants.ObjectTypes.Document, true);
}

View File

@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.Services.Navigation;
public interface IDocumentNavigationManagementService : INavigationManagementService, IRecycleBinNavigationManagementService
{
}

View File

@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.Services.Navigation;
public interface IDocumentNavigationQueryService : INavigationQueryService, IRecycleBinNavigationQueryService
{
}

View File

@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.Services.Navigation;
public interface IMediaNavigationManagementService : INavigationManagementService, IRecycleBinNavigationManagementService
{
}

View File

@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.Services.Navigation;
public interface IMediaNavigationQueryService : INavigationQueryService, IRecycleBinNavigationQueryService
{
}

View File

@@ -0,0 +1,57 @@
namespace Umbraco.Cms.Core.Services.Navigation;
/// <summary>
/// Placeholder for sharing logic between the document and media navigation services
/// for managing the navigation structure.
/// </summary>
public interface INavigationManagementService
{
/// <summary>
/// Rebuilds the entire navigation structure by refreshing the navigation tree based
/// on the current state of the underlying repository.
/// </summary>
Task RebuildAsync();
/// <summary>
/// Removes a node from the main navigation structure and moves it, along with
/// its descendants, to the root of the recycle bin structure.
/// </summary>
/// <param name="key">The unique identifier of the node to remove.</param>
/// <returns>
/// <c>true</c> if the node and its descendants were successfully removed from the
/// main navigation structure and added to the recycle bin; otherwise, <c>false</c>.
/// </returns>
bool MoveToBin(Guid key);
/// <summary>
/// Adds a new node to the main navigation structure. If a parent key is provided,
/// the new node is added as a child of the specified parent. If no parent key is
/// provided, the new node is added at the root level.
/// </summary>
/// <param name="key">The unique identifier of the new node to add.</param>
/// <param name="parentKey">
/// The unique identifier of the parent node. If <c>null</c>, the new node will be added to
/// the root level.
/// </param>
/// <returns>
/// <c>true</c> if the node was successfully added to the main navigation structure;
/// otherwise, <c>false</c>.
/// </returns>
bool Add(Guid key, Guid? parentKey = null);
/// <summary>
/// Moves an existing node to a new parent in the main navigation structure. If a
/// target parent key is provided, the node is moved under the specified parent.
/// If no target parent key is provided, the node is moved to the root level.
/// </summary>
/// <param name="key">The unique identifier of the node to move.</param>
/// <param name="targetParentKey">
/// The unique identifier of the new parent node. If <c>null</c>, the node will be moved to
/// the root level.
/// </param>
/// <returns>
/// <c>true</c> if the node and its descendants were successfully moved to the new parent
/// in the main navigation structure; otherwise, <c>false</c>.
/// </returns>
bool Move(Guid key, Guid? targetParentKey = null);
}

View File

@@ -0,0 +1,18 @@
namespace Umbraco.Cms.Core.Services.Navigation;
/// <summary>
/// Placeholder for sharing logic between the document and media navigation services
/// for querying the navigation structure.
/// </summary>
public interface INavigationQueryService
{
bool TryGetParentKey(Guid childKey, out Guid? parentKey);
bool TryGetChildrenKeys(Guid parentKey, out IEnumerable<Guid> childrenKeys);
bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable<Guid> descendantsKeys);
bool TryGetAncestorsKeys(Guid childKey, out IEnumerable<Guid> ancestorsKeys);
bool TryGetSiblingsKeys(Guid key, out IEnumerable<Guid> siblingsKeys);
}

View File

@@ -0,0 +1,40 @@
namespace Umbraco.Cms.Core.Services.Navigation;
/// <summary>
/// Placeholder for sharing logic between the document and media navigation services
/// for managing the recycle bin navigation structure.
/// </summary>
public interface IRecycleBinNavigationManagementService
{
/// <summary>
/// Rebuilds the recycle bin navigation structure by fetching the latest trashed nodes
/// from the underlying repository.
/// </summary>
Task RebuildBinAsync();
/// <summary>
/// Permanently removes a node and all of its descendants from the recycle bin navigation structure.
/// </summary>
/// <param name="key">The unique identifier of the node to remove.</param>
/// <returns>
/// <c>true</c> if the node and its descendants were successfully removed from the recycle bin;
/// otherwise, <c>false</c>.
/// </returns>
bool RemoveFromBin(Guid key);
/// <summary>
/// Restores a node and all of its descendants from the recycle bin navigation structure and moves them back
/// to the main navigation structure. The node can be restored to a specified target parent or to the root
/// level if no parent is specified.
/// </summary>
/// <param name="key">The unique identifier of the node to restore from the recycle bin navigation structure.</param>
/// <param name="targetParentKey">
/// The unique identifier of the target parent node in the main navigation structure to which the node
/// should be restored. If <c>null</c>, the node will be restored to the root level.
/// </param>
/// <returns>
/// <c>true</c> if the node and its descendants were successfully restored to the main navigation structure;
/// otherwise, <c>false</c>.
/// </returns>
bool RestoreFromBin(Guid key, Guid? targetParentKey = null);
}

View File

@@ -0,0 +1,18 @@
namespace Umbraco.Cms.Core.Services.Navigation;
/// <summary>
/// Placeholder for sharing logic between the document and media navigation services
/// for querying the recycle bin navigation structure.
/// </summary>
public interface IRecycleBinNavigationQueryService
{
bool TryGetParentKeyInBin(Guid childKey, out Guid? parentKey);
bool TryGetChildrenKeysInBin(Guid parentKey, out IEnumerable<Guid> childrenKeys);
bool TryGetDescendantsKeysInBin(Guid parentKey, out IEnumerable<Guid> descendantsKeys);
bool TryGetAncestorsKeysInBin(Guid childKey, out IEnumerable<Guid> ancestorsKeys);
bool TryGetSiblingsKeysInBin(Guid key, out IEnumerable<Guid> siblingsKeys);
}

View File

@@ -0,0 +1,18 @@
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
namespace Umbraco.Cms.Core.Services.Navigation;
internal sealed class MediaNavigationService : ContentNavigationServiceBase, IMediaNavigationQueryService, IMediaNavigationManagementService
{
public MediaNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository)
: base(coreScopeProvider, navigationRepository)
{
}
public override async Task RebuildAsync()
=> await HandleRebuildAsync(Constants.Locks.MediaTree, Constants.ObjectTypes.Media, false);
public override async Task RebuildBinAsync()
=> await HandleRebuildAsync(Constants.Locks.MediaTree, Constants.ObjectTypes.Media, true);
}

View File

@@ -0,0 +1,37 @@
using Microsoft.Extensions.Hosting;
namespace Umbraco.Cms.Core.Services.Navigation;
/// <summary>
/// Responsible for seeding the in-memory navigation structures at application's startup
/// by rebuild the navigation structures.
/// </summary>
public sealed class NavigationInitializationHostedService : IHostedLifecycleService
{
private readonly IDocumentNavigationManagementService _documentNavigationManagementService;
private readonly IMediaNavigationManagementService _mediaNavigationManagementService;
public NavigationInitializationHostedService(IDocumentNavigationManagementService documentNavigationManagementService, IMediaNavigationManagementService mediaNavigationManagementService)
{
_documentNavigationManagementService = documentNavigationManagementService;
_mediaNavigationManagementService = mediaNavigationManagementService;
}
public async Task StartingAsync(CancellationToken cancellationToken)
{
await _documentNavigationManagementService.RebuildAsync();
await _documentNavigationManagementService.RebuildBinAsync();
await _mediaNavigationManagementService.RebuildAsync();
await _mediaNavigationManagementService.RebuildBinAsync();
}
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}