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;