POC to normalize all event entities when we track events so that all args contain the latest (non stale) entity
This commit is contained in:
@@ -5,81 +5,126 @@ using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.EntityBase;
|
||||
|
||||
namespace Umbraco.Core.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Event args for a strongly typed object that can support cancellation
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
[HostProtection(SecurityAction.LinkDemand, SharedState = true)]
|
||||
public class CancellableObjectEventArgs<T> : CancellableEventArgs, IEquatable<CancellableObjectEventArgs<T>>
|
||||
{
|
||||
public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages messages, IDictionary<string, object> additionalData)
|
||||
{
|
||||
/// <summary>
|
||||
/// Used as a base class for the generic type CancellableObjectEventArgs{T} so that we can get direct 'object' access to the underlying EventObject
|
||||
/// </summary>
|
||||
[HostProtection(SecurityAction.LinkDemand, SharedState = true)]
|
||||
public abstract class CancellableObjectEventArgs : CancellableEventArgs
|
||||
{
|
||||
protected CancellableObjectEventArgs(object eventObject, bool canCancel, EventMessages messages, IDictionary<string, object> 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)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the object relating to the event
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is protected so that inheritors can expose it with their own name
|
||||
/// </remarks>
|
||||
protected T EventObject { get; set; }
|
||||
/// <summary>
|
||||
/// Returns the object relating to the event
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is protected so that inheritors can expose it with their own name
|
||||
/// </remarks>
|
||||
internal object EventObject { get; set; }
|
||||
|
||||
public bool Equals(CancellableObjectEventArgs<T> other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
return base.Equals(other) && EqualityComparer<T>.Default.Equals(EventObject, other.EventObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for a strongly typed object that can support cancellation
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
[HostProtection(SecurityAction.LinkDemand, SharedState = true)]
|
||||
public class CancellableObjectEventArgs<T> : CancellableObjectEventArgs, IEquatable<CancellableObjectEventArgs<T>>
|
||||
{
|
||||
public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages messages, IDictionary<string, object> 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<T>) obj);
|
||||
}
|
||||
public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages eventMessages)
|
||||
: base(eventObject, canCancel, eventMessages)
|
||||
{
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (base.GetHashCode() * 397) ^ EqualityComparer<T>.Default.GetHashCode(EventObject);
|
||||
}
|
||||
}
|
||||
public CancellableObjectEventArgs(T eventObject, EventMessages eventMessages)
|
||||
: base(eventObject, eventMessages)
|
||||
{
|
||||
}
|
||||
|
||||
public static bool operator ==(CancellableObjectEventArgs<T> left, CancellableObjectEventArgs<T> right)
|
||||
{
|
||||
return Equals(left, right);
|
||||
}
|
||||
public CancellableObjectEventArgs(T eventObject, bool canCancel)
|
||||
: base(eventObject, canCancel)
|
||||
{
|
||||
}
|
||||
|
||||
public static bool operator !=(CancellableObjectEventArgs<T> left, CancellableObjectEventArgs<T> right)
|
||||
{
|
||||
return !Equals(left, right);
|
||||
}
|
||||
}
|
||||
public CancellableObjectEventArgs(T eventObject)
|
||||
: base(eventObject)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the object relating to the event
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is protected so that inheritors can expose it with their own name
|
||||
/// </remarks>
|
||||
protected new T EventObject
|
||||
{
|
||||
get { return (T)base.EventObject; }
|
||||
set { base.EventObject = value; }
|
||||
}
|
||||
|
||||
public bool Equals(CancellableObjectEventArgs<T> other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
return base.Equals(other) && EqualityComparer<T>.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<T>)obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (base.GetHashCode() * 397) ^ EqualityComparer<T>.Default.GetHashCode(EventObject);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool operator ==(CancellableObjectEventArgs<T> left, CancellableObjectEventArgs<T> right)
|
||||
{
|
||||
return Equals(left, right);
|
||||
}
|
||||
|
||||
public static bool operator !=(CancellableObjectEventArgs<T> left, CancellableObjectEventArgs<T> right)
|
||||
{
|
||||
return !Equals(left, right);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IEventDefinition>();
|
||||
|
||||
|
||||
switch (filter)
|
||||
{
|
||||
case EventDefinitionFilter.All:
|
||||
UpdateToLatestEntity(_events);
|
||||
return _events;
|
||||
case EventDefinitionFilter.FirstIn:
|
||||
var l1 = new OrderedHashSet<IEventDefinition>();
|
||||
@@ -75,6 +78,7 @@ namespace Umbraco.Core.Events
|
||||
{
|
||||
l1.Add(e);
|
||||
}
|
||||
UpdateToLatestEntity(l1);
|
||||
return l1;
|
||||
case EventDefinitionFilter.LastIn:
|
||||
var l2 = new OrderedHashSet<IEventDefinition>(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<IEventDefinition> events)
|
||||
{
|
||||
//used to keep the 'latest' entity
|
||||
var allEntities = new List<IEntity>();
|
||||
var cancelableArgs = new List<CancellableObjectEventArgs>();
|
||||
|
||||
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<IEntity>(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;
|
||||
|
||||
@@ -166,8 +166,8 @@ namespace Umbraco.Core
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [is of generic type] [the specified type].
|
||||
/// </summary>
|
||||
|
||||
@@ -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<Type, FieldInfo[]>();
|
||||
|
||||
private static readonly Assembly[] EmptyAssemblies = new Assembly[0];
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <returns></returns>
|
||||
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<T> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the method is actually overriding a base method
|
||||
/// </summary>
|
||||
|
||||
@@ -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<string>), typeof (SaveEventArgs<int>), typeof (SaveEventArgs<decimal>) };
|
||||
var knownArgTypes = new[] { typeof(SaveEventArgs<string>), typeof(SaveEventArgs<int>), typeof(SaveEventArgs<decimal>) };
|
||||
|
||||
for (var i = 0; i < events.Length; i++)
|
||||
{
|
||||
@@ -89,6 +92,51 @@ namespace Umbraco.Tests.Scoping
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
[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<IDatabaseFactory2>());
|
||||
using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher()))
|
||||
{
|
||||
|
||||
scope.Events.Dispatch(DoThingForContent, this, new SaveEventArgs<IContent>(content1));
|
||||
scope.Events.Dispatch(DoThingForContent, this, new SaveEventArgs<IContent>(content2));
|
||||
scope.Events.Dispatch(DoThingForContent, this, new SaveEventArgs<IContent>(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<IContent>)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<SaveEventArgs<IContent>> DoThingForContent;
|
||||
|
||||
public static event EventHandler<SaveEventArgs<string>> DoThing1;
|
||||
|
||||
public static event EventHandler<SaveEventArgs<int>> DoThing2;
|
||||
|
||||
Reference in New Issue
Block a user