diff --git a/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs b/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs index 00a33c0b6c52..7037e1bea897 100644 --- a/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs +++ b/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using Moq; @@ -10,6 +11,7 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; +using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; @@ -168,17 +170,73 @@ public void CanHandleEvent() } [Test] - public void OnlyHandlesOnContentTypeEvent() + public void GroupsContentTypeEvents() { - var definitions = new IEventDefinition[] + var num = 30; + var contentTypes = Enumerable.Repeat(MockedContentTypes.CreateBasicContentType(), num); + var mediaTypes = Enumerable.Repeat(MockedContentTypes.CreateImageMediaType(), num); + var memberTypes = Enumerable.Repeat(MockedContentTypes.CreateSimpleMemberType(), num); + var definitionsContent = contentTypes.SelectMany(x => new IEventDefinition[] { - new EventDefinition.EventArgs>(null, Current.Services.ContentTypeService, new ContentTypeChange.EventArgs(Enumerable.Empty>()), "Changed"), - new EventDefinition>(null, Current.Services.ContentTypeService, new SaveEventArgs(Enumerable.Empty()), "Saved"), - new EventDefinition.EventArgs>(null, Current.Services.ContentTypeService, new ContentTypeChange.EventArgs(Enumerable.Empty>()), "Changed"), - new EventDefinition>(null, Current.Services.ContentTypeService, new SaveEventArgs(Enumerable.Empty()), "Saved"), - }; - var result = DistributedCacheBinder.GetReducedEventList(definitions); - Assert.AreEqual(1, result.Count()); + new EventDefinition.EventArgs>(null, Current.Services.ContentTypeService, new ContentTypeChange.EventArgs(new ContentTypeChange(x, ContentTypeChangeTypes.Create)), "Changed"), + new EventDefinition>(null, Current.Services.ContentTypeService, new SaveEventArgs(x), "Saved"), + }); + + var definitionsMedia = mediaTypes.SelectMany(x => new IEventDefinition[] + { + new EventDefinition.EventArgs>(null, Current.Services.MediaTypeService, new ContentTypeChange.EventArgs(new ContentTypeChange(x, ContentTypeChangeTypes.Create)), "Changed"), + new EventDefinition>(null, Current.Services.MediaTypeService, new SaveEventArgs(x), "Saved"), + }); + var definitionsMember = memberTypes.SelectMany(x => new IEventDefinition[] + { + new EventDefinition.EventArgs>(null, Current.Services.MemberTypeService, new ContentTypeChange.EventArgs(new ContentTypeChange(x, ContentTypeChangeTypes.Create)), "Changed"), + new EventDefinition>(null, Current.Services.MemberTypeService, new SaveEventArgs(x), "Saved"), + }); + + var definitions = new List(); + definitions.AddRange(definitionsContent); + definitions.AddRange(definitionsMedia); + definitions.AddRange(definitionsMember); + + var result = DistributedCacheBinder.GetGroupedEventList(definitions); + + Assert.Multiple(() => + { + Assert.AreEqual(num * 6, definitions.Count(), "Precondition is we have many definitions"); + Assert.AreEqual(6, result.Count(), "Unexpected number of reduced definitions"); + foreach (var eventDefinition in result) + { + if (eventDefinition.Args is SaveEventArgs saveContentEventArgs) + { + Assert.AreEqual(num, saveContentEventArgs.SavedEntities.Count()); + } + + if (eventDefinition.Args is ContentTypeChange.EventArgs changeContentEventArgs) + { + Assert.AreEqual(num, changeContentEventArgs.Changes.Count()); + } + + if (eventDefinition.Args is SaveEventArgs saveMediaEventArgs) + { + Assert.AreEqual(num, saveMediaEventArgs.SavedEntities.Count()); + } + + if (eventDefinition.Args is ContentTypeChange.EventArgs changeMediaEventArgs) + { + Assert.AreEqual(num, changeMediaEventArgs.Changes.Count()); + } + + if (eventDefinition.Args is SaveEventArgs saveMemberEventArgs) + { + Assert.AreEqual(num, saveMemberEventArgs.SavedEntities.Count()); + } + + if (eventDefinition.Args is ContentTypeChange.EventArgs changeMemberEventArgs) + { + Assert.AreEqual(num, changeMemberEventArgs.Changes.Count()); + } + } + }); } } } diff --git a/src/Umbraco.Web/Cache/DistributedCacheBinder.cs b/src/Umbraco.Web/Cache/DistributedCacheBinder.cs index e3a5a01d54d8..bfb1a01a6919 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheBinder.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheBinder.cs @@ -6,6 +6,9 @@ using Umbraco.Core; using Umbraco.Core.Events; using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Changes; namespace Umbraco.Web.Cache { @@ -66,10 +69,9 @@ public void HandleEvents(IEnumerable events) using (_umbracoContextFactory.EnsureUmbracoContext()) { // When it comes to content types types, a change to any single one will trigger a reload of the content and media caches. - // As far as I (AB) can tell, there's no type specific logic here, they all clear caches for all content types, and trigger a reload of all content and media. - // We also have events registered for Changed and Saved, which do the same thing, so really only need one of these. - // Hence if we have more than one document or media types, we can and should only handle one of the events for one, to avoid repeated cache reloads. - foreach (var e in GetReducedEventList(events)) + // We can reduce the impact of that by grouping the events to invoke just one per type, providing a collection of the individual arguments. + var groupedEvents = GetGroupedEventList(events); + foreach (var e in groupedEvents) { var handler = FindHandler(e); if (handler == null) @@ -86,47 +88,77 @@ public void HandleEvents(IEnumerable events) } // Internal for tests - internal static IEnumerable GetReducedEventList(IEnumerable events) + internal static IEnumerable GetGroupedEventList(IEnumerable events) { - var reducedEvents = new List(); + var groupedEvents = new List(); - var gotDoumentType = false; - var gotMediaType = false; - var gotMemberType = false; + var grouped = events.GroupBy(x => x.GetType()); - foreach (var evt in events) + foreach (var group in grouped) { - if (evt.Sender.ToString().Contains(nameof(Core.Services.Implement.ContentTypeService))) + if (group.Key == typeof(EventDefinition>)) { - if (gotDoumentType == false) - { - reducedEvents.Add(evt); - gotDoumentType = true; - } + GroupSaveEvents(groupedEvents, group); } - else if (evt.Sender.ToString().Contains(nameof(Core.Services.Implement.MediaTypeService))) + else if (group.Key == typeof(EventDefinition.EventArgs>)) { - if (gotMediaType == false) - { - reducedEvents.Add(evt); - gotMediaType = true; - } + GroupChangeEvents(groupedEvents, group); } - else if (evt.Sender.ToString().Contains(nameof(Core.Services.Implement.MemberTypeService))) + else if (group.Key == typeof(EventDefinition>)) { - if (gotMemberType == false) - { - reducedEvents.Add(evt); - gotMemberType = true; - } + GroupSaveEvents(groupedEvents, group); + } + else if (group.Key == typeof(EventDefinition.EventArgs>)) + { + GroupChangeEvents(groupedEvents, group); + } + else if (group.Key == typeof(EventDefinition>)) + { + GroupSaveEvents(groupedEvents, group); + } + else if (group.Key == typeof(EventDefinition.EventArgs>)) + { + GroupChangeEvents(groupedEvents, group); } else { - reducedEvents.Add(evt); + groupedEvents.AddRange(group); } } - return reducedEvents; + return groupedEvents; + } + + private static void GroupSaveEvents(List groupedEvents, IGrouping group) + where TService : IContentTypeBaseService + where TType : IContentTypeBase + { + var groupedGroups = group.GroupBy(x => (x.EventName, x.Sender)); + + foreach (var groupedGroup in groupedGroups) + { + groupedEvents.Add(new EventDefinition>( + null, + (TService)groupedGroup.Key.Sender, + new SaveEventArgs(groupedGroup.SelectMany(x => ((SaveEventArgs)x.Args).SavedEntities)), + groupedGroup.Key.EventName)); + } + } + + private static void GroupChangeEvents(List groupedEvents, IGrouping group) + where TService : IContentTypeBaseService + where TType : class, IContentTypeComposition + { + var groupedGroups = group.GroupBy(x => (x.EventName, x.Sender)); + + foreach (var groupedGroup in groupedGroups) + { + groupedEvents.Add(new EventDefinition.EventArgs>( + null, + (TService)groupedGroup.Key.Sender, + new ContentTypeChange.EventArgs(groupedGroup.SelectMany(x => ((ContentTypeChange.EventArgs)x.Args).Changes)), + groupedGroup.Key.EventName)); + } } } }