From 9cd7e0998669d8b6e1447a504d38bc7ea25872f8 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 28 Apr 2017 10:04:43 +1000 Subject: [PATCH] POC to normalize all event entities when we track events so that all args contain the latest (non stale) entity --- .../Events/CancellableObjectEventArgs.cs | 159 +++++++++++------- .../Events/ScopeEventDispatcherBase.cs | 91 +++++++++- src/Umbraco.Core/TypeExtensions.cs | 4 +- src/Umbraco.Core/TypeHelper.cs | 40 ++++- .../Scoping/ScopeEventDispatcherTests.cs | 54 +++++- 5 files changed, 285 insertions(+), 63 deletions(-) diff --git a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs index 7815747f18..6787e6aa7a 100644 --- a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs @@ -5,81 +5,126 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Events -{ - /// - /// Event args for a strongly typed object that can support cancellation - /// - /// - [HostProtection(SecurityAction.LinkDemand, SharedState = true)] - public class CancellableObjectEventArgs : CancellableEventArgs, IEquatable> - { - public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) +{ + /// + /// Used as a base class for the generic type CancellableObjectEventArgs{T} so that we can get direct 'object' access to the underlying EventObject + /// + [HostProtection(SecurityAction.LinkDemand, SharedState = true)] + public abstract class CancellableObjectEventArgs : CancellableEventArgs + { + protected CancellableObjectEventArgs(object eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) : base(canCancel, messages, additionalData) - { + { EventObject = eventObject; } - public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages eventMessages) + protected CancellableObjectEventArgs(object eventObject, bool canCancel, EventMessages eventMessages) : base(canCancel, eventMessages) { EventObject = eventObject; } - public CancellableObjectEventArgs(T eventObject, EventMessages eventMessages) + protected CancellableObjectEventArgs(object eventObject, EventMessages eventMessages) : this(eventObject, true, eventMessages) { } - public CancellableObjectEventArgs(T eventObject, bool canCancel) - : base(canCancel) - { - EventObject = eventObject; - } + protected CancellableObjectEventArgs(object eventObject, bool canCancel) + : base(canCancel) + { + EventObject = eventObject; + } - public CancellableObjectEventArgs(T eventObject) - : this(eventObject, true) - { - } + protected CancellableObjectEventArgs(object eventObject) + : this(eventObject, true) + { + } - /// - /// Returns the object relating to the event - /// - /// - /// This is protected so that inheritors can expose it with their own name - /// - protected T EventObject { get; set; } + /// + /// Returns the object relating to the event + /// + /// + /// This is protected so that inheritors can expose it with their own name + /// + internal object EventObject { get; set; } - public bool Equals(CancellableObjectEventArgs other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && EqualityComparer.Default.Equals(EventObject, other.EventObject); - } + } + + /// + /// Event args for a strongly typed object that can support cancellation + /// + /// + [HostProtection(SecurityAction.LinkDemand, SharedState = true)] + public class CancellableObjectEventArgs : CancellableObjectEventArgs, IEquatable> + { + public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(eventObject, canCancel, messages, additionalData) + { + } - 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((CancellableObjectEventArgs) obj); - } + public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { + } - public override int GetHashCode() - { - unchecked - { - return (base.GetHashCode() * 397) ^ EqualityComparer.Default.GetHashCode(EventObject); - } - } + public CancellableObjectEventArgs(T eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + } - public static bool operator ==(CancellableObjectEventArgs left, CancellableObjectEventArgs right) - { - return Equals(left, right); - } + public CancellableObjectEventArgs(T eventObject, bool canCancel) + : base(eventObject, canCancel) + { + } - public static bool operator !=(CancellableObjectEventArgs left, CancellableObjectEventArgs right) - { - return !Equals(left, right); - } - } + public CancellableObjectEventArgs(T eventObject) + : base(eventObject) + { + } + + /// + /// Returns the object relating to the event + /// + /// + /// This is protected so that inheritors can expose it with their own name + /// + protected new T EventObject + { + get { return (T)base.EventObject; } + set { base.EventObject = value; } + } + + public bool Equals(CancellableObjectEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && EqualityComparer.Default.Equals(EventObject, other.EventObject); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((CancellableObjectEventArgs)obj); + } + + public override int GetHashCode() + { + unchecked + { + 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); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/ScopeEventDispatcherBase.cs b/src/Umbraco.Core/Events/ScopeEventDispatcherBase.cs index d8462d18b3..14990cad73 100644 --- a/src/Umbraco.Core/Events/ScopeEventDispatcherBase.cs +++ b/src/Umbraco.Core/Events/ScopeEventDispatcherBase.cs @@ -1,6 +1,8 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Events { @@ -64,10 +66,11 @@ namespace Umbraco.Core.Events { if (_events == null) return Enumerable.Empty(); - + switch (filter) { case EventDefinitionFilter.All: + UpdateToLatestEntity(_events); return _events; case EventDefinitionFilter.FirstIn: var l1 = new OrderedHashSet(); @@ -75,6 +78,7 @@ namespace Umbraco.Core.Events { l1.Add(e); } + UpdateToLatestEntity(l1); return l1; case EventDefinitionFilter.LastIn: var l2 = new OrderedHashSet(keepOldest: false); @@ -82,12 +86,97 @@ namespace Umbraco.Core.Events { l2.Add(e); } + UpdateToLatestEntity(l2); return l2; default: throw new ArgumentOutOfRangeException("filter", filter, null); } } + private void UpdateToLatestEntity(IEnumerable events) + { + //used to keep the 'latest' entity + var allEntities = new List(); + var cancelableArgs = new List(); + + foreach (var eventDefinition in events) + { + var args = eventDefinition.Args as CancellableObjectEventArgs; + if (args != null) + { + cancelableArgs.Add(args); + + var list = TypeHelper.CreateGenericEnumerableFromOjbect(args.EventObject); + + if (list == null) + { + //extract the event object + var obj = args.EventObject as IEntity; + if (obj != null) + { + allEntities.Add(obj); + } + } + else + { + foreach (var entity in list) + { + //extract the event object + var obj = entity as IEntity; + if (obj != null) + { + allEntities.Add(obj); + } + } + } + } + } + + var latestEntities = new OrderedHashSet(keepOldest: true); + foreach (var entity in allEntities.OrderByDescending(entity => entity.UpdateDate)) + { + latestEntities.Add(entity); + } + + foreach (var args in cancelableArgs) + { + var list = TypeHelper.CreateGenericEnumerableFromOjbect(args.EventObject); + if (list == null) + { + //try to find the args entity in the latest entity - based on the equality operators, this will + //match by Id since that is the default equality checker for IEntity. If one is found, than it is + //the most recent entity instance so update the args with that instance so we don't emit a stale instance. + var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, args.EventObject)); + if (foundEntity != null) + { + args.EventObject = foundEntity; + } + } + else + { + var updated = false; + + for (int i = 0; i < list.Count; i++) + { + //try to find the args entity in the latest entity - based on the equality operators, this will + //match by Id since that is the default equality checker for IEntity. If one is found, than it is + //the most recent entity instance so update the args with that instance so we don't emit a stale instance. + var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, list[i])); + if (foundEntity != null) + { + list[i] = foundEntity; + updated = true; + } + } + + if (updated) + { + args.EventObject = list; + } + } + } + } + public void ScopeExit(bool completed) { if (_events == null) return; diff --git a/src/Umbraco.Core/TypeExtensions.cs b/src/Umbraco.Core/TypeExtensions.cs index 76dc79c219..84d2a2d47a 100644 --- a/src/Umbraco.Core/TypeExtensions.cs +++ b/src/Umbraco.Core/TypeExtensions.cs @@ -166,8 +166,8 @@ namespace Umbraco.Core return true; } return false; - } - + } + /// /// Determines whether [is of generic type] [the specified type]. /// diff --git a/src/Umbraco.Core/TypeHelper.cs b/src/Umbraco.Core/TypeHelper.cs index 14c441dcd5..1a2d1df6f5 100644 --- a/src/Umbraco.Core/TypeHelper.cs +++ b/src/Umbraco.Core/TypeHelper.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Reflection; @@ -19,7 +20,44 @@ namespace Umbraco.Core = new ConcurrentDictionary(); private static readonly Assembly[] EmptyAssemblies = new Assembly[0]; - + + /// + /// Based on a type we'll check if it is IEnumerable{T} (or similar) and if so we'll return a List{T}, this will also deal with array types and return List{T} for those too. + /// If it cannot be done, null is returned. + /// + /// + /// + internal static IList CreateGenericEnumerableFromOjbect(object obj) + { + var type = obj.GetType(); + + if (type.IsGenericType) + { + var genericTypeDef = type.GetGenericTypeDefinition(); + + if (genericTypeDef == typeof(IEnumerable<>) + || genericTypeDef == typeof(ICollection<>) + || genericTypeDef == typeof(Collection<>) + || genericTypeDef == typeof(IList<>) + || genericTypeDef == typeof(List<>)) + { + //if it is a IEnumerable<>, IList or ICollection<> we'll use a List<> + var genericType = typeof(List<>).MakeGenericType(type.GetGenericArguments()); + //pass in obj to fill the list + return (IList)Activator.CreateInstance(genericType, obj); + } + } + if (type.IsArray) + { + //if its an array, we'll use a List<> + var genericType = typeof(List<>).MakeGenericType(type.GetElementType()); + //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 /// diff --git a/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs b/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs index b68e2a3f9f..a4e48c07fe 100644 --- a/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs @@ -4,13 +4,16 @@ using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Core.Events; +using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Scoping; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Entities; namespace Umbraco.Tests.Scoping { [TestFixture] - public class ScopeEventDispatcherTests + public class ScopeEventDispatcherTests : BaseUmbracoConfigurationTest { [SetUp] public void Setup() @@ -79,7 +82,7 @@ namespace Umbraco.Tests.Scoping var events = scope.Events.GetEvents(EventDefinitionFilter.All).ToArray(); var knownNames = new[] { "DoThing1", "DoThing2", "DoThing3" }; - var knownArgTypes = new[] { typeof (SaveEventArgs), typeof (SaveEventArgs), typeof (SaveEventArgs) }; + var knownArgTypes = new[] { typeof(SaveEventArgs), typeof(SaveEventArgs), typeof(SaveEventArgs) }; for (var i = 0; i < events.Length; i++) { @@ -89,6 +92,51 @@ namespace Umbraco.Tests.Scoping } } + /// + /// This will test that when we track events that before we Get the events we normalize all of the + /// event entities to be the latest one (most current) found amongst the event so that there is + /// no 'stale' entities in any of the args + /// + [Test] + public void LatestEntities() + { + DoThingForContent += OnDoThingFail; + + var now = DateTime.Now; + var contentType = MockedContentTypes.CreateBasicContentType(); + var content1 = MockedContent.CreateBasicContent(contentType); + content1.Id = 123; + content1.UpdateDate = now.AddMinutes(1); + var content2 = MockedContent.CreateBasicContent(contentType); + content2.Id = 123; + content1.UpdateDate = now.AddMinutes(2); + var content3 = MockedContent.CreateBasicContent(contentType); + content3.Id = 123; + content1.UpdateDate = now.AddMinutes(3); + + var scopeProvider = new ScopeProvider(Mock.Of()); + using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) + { + + scope.Events.Dispatch(DoThingForContent, this, new SaveEventArgs(content1)); + scope.Events.Dispatch(DoThingForContent, this, new SaveEventArgs(content2)); + scope.Events.Dispatch(DoThingForContent, this, new SaveEventArgs(content3)); + + // events have been queued + var events = scope.Events.GetEvents(EventDefinitionFilter.All).ToArray(); + Assert.AreEqual(3, events.Length); + + foreach (var t in events) + { + var args = (SaveEventArgs)t.Args; + foreach (var entity in args.SavedEntities) + { + Assert.AreEqual(content3, entity); + } + } + } + } + [TestCase(true)] [TestCase(false)] public void EventsDispatching_Passive(bool complete) @@ -177,6 +225,8 @@ namespace Umbraco.Tests.Scoping Assert.Fail(); } + public static event EventHandler> DoThingForContent; + public static event EventHandler> DoThing1; public static event EventHandler> DoThing2;