using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.PublishedCache { /// /// Subscribes to Umbraco events to ensure nucache remains consistent with the source data /// public class PublishedSnapshotServiceEventHandler : IDisposable, INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler { private readonly IRuntimeState _runtime; private bool _disposedValue; private readonly IPublishedSnapshotService _publishedSnapshotService; private readonly INuCacheContentService _publishedContentService; private readonly IContentService _contentService; private readonly IMediaService _mediaService; /// /// Initializes a new instance of the class. /// public PublishedSnapshotServiceEventHandler( IRuntimeState runtime, IPublishedSnapshotService publishedSnapshotService, INuCacheContentService publishedContentService, IContentService contentService, IMediaService mediaService) { _runtime = runtime; _publishedSnapshotService = publishedSnapshotService; _publishedContentService = publishedContentService; _contentService = contentService; _mediaService = mediaService; } /// /// Binds to the Umbraco events /// /// Returns true if binding occurred public bool Initialize() { // however, the cache is NOT available until we are configured, because loading // content (and content types) from database cannot be consistent (see notes in "Handle // Notifications" region), so // - notifications will be ignored // - trying to obtain a published snapshot from the service will throw if (_runtime.Level != RuntimeLevel.Run) { return false; } return true; } // note: if the service is not ready, ie _isReady is false, then we still handle repository events, // because we can, we do not need a working published snapshot to do it - the only reason why it could cause an // issue is if the database table is not ready, but that should be prevented by migrations. // we need them to be "repository" events ie to trigger from within the repository transaction, // because they need to be consistent with the content that is being refreshed/removed - and that // should be guaranteed by a DB transaction public void Handle(ScopedEntityRemoveNotification notification) => _publishedContentService.DeleteContentItem(notification.Entity); public void Handle(MemberDeletingNotification notification) => _publishedContentService.DeleteContentItems(notification.DeletedEntities); public void Handle(MemberRefreshNotification notification) => _publishedContentService.RefreshMember(notification.Entity); public void Handle(MediaRefreshNotification notification) => _publishedContentService.RefreshMedia(notification.Entity); public void Handle(ContentRefreshNotification notification) => _publishedContentService.RefreshContent(notification.Entity); public void Handle(ContentTypeRefreshedNotification notification) { const ContentTypeChangeTypes types // only for those that have been refreshed = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; var contentTypeIds = notification.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray(); if (contentTypeIds.Any()) { _publishedSnapshotService.Rebuild(contentTypeIds: contentTypeIds); } } public void Handle(MediaTypeRefreshedNotification notification) { const ContentTypeChangeTypes types // only for those that have been refreshed = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; var mediaTypeIds = notification.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray(); if (mediaTypeIds.Any()) { _publishedSnapshotService.Rebuild(mediaTypeIds: mediaTypeIds); } } public void Handle(MemberTypeRefreshedNotification notification) { const ContentTypeChangeTypes types // only for those that have been refreshed = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; var memberTypeIds = notification.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray(); if (memberTypeIds.Any()) { _publishedSnapshotService.Rebuild(memberTypeIds: memberTypeIds); } } // TODO: This should be a cache refresher call! /// /// If a is ever saved with a different culture, we need to rebuild all of the content nucache database table /// public void Handle(LanguageSavedNotification notification) { // culture changed on an existing language var cultureChanged = notification.SavedEntities.Any(x => !x.WasPropertyDirty(nameof(ILanguage.Id)) && x.WasPropertyDirty(nameof(ILanguage.IsoCode))); if (cultureChanged) { // Rebuild all content for all content types _publishedSnapshotService.Rebuild(contentTypeIds: Array.Empty()); } } protected virtual void Dispose(bool disposing) { if (!_disposedValue) { _disposedValue = true; } } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } } }