Adds ability to supersede events so we don't have an issue of saved event being raised for an entity after it's been deleted.
This commit is contained in:
@@ -3,6 +3,10 @@ using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Events
|
||||
{
|
||||
[SupersedeEvent(typeof(SaveEventArgs<>))]
|
||||
[SupersedeEvent(typeof(PublishEventArgs<>))]
|
||||
[SupersedeEvent(typeof(MoveEventArgs<>))]
|
||||
[SupersedeEvent(typeof(CopyEventArgs<>))]
|
||||
public class DeleteEventArgs<TEntity> : CancellableObjectEventArgs<IEnumerable<TEntity>>, IEquatable<DeleteEventArgs<TEntity>>, IDeletingMediaFilesEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace Umbraco.Core.Events
|
||||
{
|
||||
public abstract class ScopeEventDispatcherBase : IEventDispatcher
|
||||
{
|
||||
//events will be enlisted in the order they are raised
|
||||
private List<IEventDefinition> _events;
|
||||
private readonly bool _raiseCancelable;
|
||||
|
||||
@@ -70,72 +71,161 @@ namespace Umbraco.Core.Events
|
||||
switch (filter)
|
||||
{
|
||||
case EventDefinitionFilter.All:
|
||||
UpdateToLatestEntity(_events);
|
||||
return _events;
|
||||
return FilterSupersededAndUpdateToLatestEntity(_events);
|
||||
case EventDefinitionFilter.FirstIn:
|
||||
var l1 = new OrderedHashSet<IEventDefinition>();
|
||||
foreach (var e in _events)
|
||||
{
|
||||
l1.Add(e);
|
||||
}
|
||||
UpdateToLatestEntity(l1);
|
||||
return l1;
|
||||
return FilterSupersededAndUpdateToLatestEntity(l1);
|
||||
case EventDefinitionFilter.LastIn:
|
||||
var l2 = new OrderedHashSet<IEventDefinition>(keepOldest: false);
|
||||
foreach (var e in _events)
|
||||
{
|
||||
l2.Add(e);
|
||||
}
|
||||
UpdateToLatestEntity(l2);
|
||||
return l2;
|
||||
return FilterSupersededAndUpdateToLatestEntity(l2);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException("filter", filter, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateToLatestEntity(IEnumerable<IEventDefinition> events)
|
||||
|
||||
private class EventDefinitionTypeData
|
||||
{
|
||||
//used to keep the 'latest' entity
|
||||
var allEntities = new List<IEntity>();
|
||||
public IEventDefinition EventDefinition { get; set; }
|
||||
public Type EventArgType { get; set; }
|
||||
public SupersedeEventAttribute[] SupersedeAttributes { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will iterate over the events (latest first) and filter out any events or entities in event args that are included
|
||||
/// in more recent events that Supersede previous ones. For example, If an Entity has been Saved and then Deleted, we don't want
|
||||
/// to raise the Saved event (well actually we just don't want to include it in the args for that saved event)
|
||||
/// </summary>
|
||||
/// <param name="events"></param>
|
||||
/// <returns></returns>
|
||||
private static IEnumerable<IEventDefinition> FilterSupersededAndUpdateToLatestEntity(IReadOnlyList<IEventDefinition> events)
|
||||
{
|
||||
//used to keep the 'latest' entity and associated event definition data
|
||||
var allEntities = new List<Tuple<IEntity, EventDefinitionTypeData>>();
|
||||
|
||||
//tracks all CancellableObjectEventArgs instances in the events which is the only type of args we can work with
|
||||
var cancelableArgs = new List<CancellableObjectEventArgs>();
|
||||
|
||||
foreach (var eventDefinition in events)
|
||||
|
||||
var result = new List<IEventDefinition>();
|
||||
|
||||
//This will eagerly load all of the event arg types and their attributes so we don't have to continuously look this data up
|
||||
var allArgTypesWithAttributes = events.Select(x => x.Args.GetType())
|
||||
.Distinct()
|
||||
.ToDictionary(x => x, x => x.GetCustomAttributes<SupersedeEventAttribute>(false).ToArray());
|
||||
|
||||
//Iterate all events and collect the actual entities in them and relates them to their corresponding EventDefinitionTypeData
|
||||
//we'll process the list in reverse because events are added in the order they are raised and we want to filter out
|
||||
//any entities from event args that are not longer relevant
|
||||
//(i.e. if an item is Deleted after it's Saved, we won't include the item in the Saved args)
|
||||
for (var index = events.Count - 1; index >= 0; index--)
|
||||
{
|
||||
var eventDefinition = events[index];
|
||||
|
||||
var argType = eventDefinition.Args.GetType();
|
||||
var attributes = allArgTypesWithAttributes[eventDefinition.Args.GetType()];
|
||||
|
||||
var meta = new EventDefinitionTypeData
|
||||
{
|
||||
EventDefinition = eventDefinition,
|
||||
EventArgType = argType,
|
||||
SupersedeAttributes = attributes
|
||||
};
|
||||
|
||||
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);
|
||||
//Now check if this entity already exists in other event args that supersede this current event arg type
|
||||
if (IsFiltered(obj, meta, allEntities) == false)
|
||||
{
|
||||
//if it's not filtered we can adde these args to the response
|
||||
cancelableArgs.Add(args);
|
||||
result.Add(eventDefinition);
|
||||
//track the entity
|
||||
allEntities.Add(Tuple.Create(obj, meta));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var toRemove = new List<IEntity>();
|
||||
foreach (var entity in list)
|
||||
{
|
||||
//extract the event object
|
||||
var obj = entity as IEntity;
|
||||
if (obj != null)
|
||||
{
|
||||
allEntities.Add(obj);
|
||||
//Now check if this entity already exists in other event args that supersede this current event arg type
|
||||
if (IsFiltered(obj, meta, allEntities))
|
||||
{
|
||||
//track it to be removed
|
||||
toRemove.Add(obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
//track the entity, it's not filtered
|
||||
allEntities.Add(Tuple.Create(obj, meta));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//remove anything that has been filtered
|
||||
foreach (var entity in toRemove)
|
||||
{
|
||||
list.Remove(entity);
|
||||
}
|
||||
|
||||
//track the event and include in the response if there's still entities remaining in the list
|
||||
if (list.Count > 0)
|
||||
{
|
||||
if (toRemove.Count > 0)
|
||||
{
|
||||
//re-assign if the items have changed
|
||||
args.EventObject = list;
|
||||
}
|
||||
cancelableArgs.Add(args);
|
||||
result.Add(eventDefinition);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//it's not a cancelable event arg so we just include it in the result
|
||||
result.Add(eventDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
//Now we'll deal with ensuring that only the latest(non stale) entities are used throughout all event args
|
||||
UpdateToLatestEntities(allEntities, cancelableArgs);
|
||||
|
||||
//we need to reverse the result since we've been adding by latest added events first!
|
||||
result.Reverse();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void UpdateToLatestEntities(IEnumerable<Tuple<IEntity, EventDefinitionTypeData>> allEntities, IEnumerable<CancellableObjectEventArgs> cancelableArgs)
|
||||
{
|
||||
//Now we'll deal with ensuring that only the latest(non stale) entities are used throughout all event args
|
||||
|
||||
var latestEntities = new OrderedHashSet<IEntity>(keepOldest: true);
|
||||
foreach (var entity in allEntities.OrderByDescending(entity => entity.UpdateDate))
|
||||
foreach (var entity in allEntities.OrderByDescending(entity => entity.Item1.UpdateDate))
|
||||
{
|
||||
latestEntities.Add(entity);
|
||||
latestEntities.Add(entity.Item1);
|
||||
}
|
||||
|
||||
foreach (var args in cancelableArgs)
|
||||
@@ -155,7 +245,7 @@ namespace Umbraco.Core.Events
|
||||
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
|
||||
@@ -177,6 +267,55 @@ namespace Umbraco.Core.Events
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will check against all of the processed entity/events (allEntities) to see if this entity already exists in
|
||||
/// event args that supersede the event args being passed in and if so returns true.
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <param name="eventDef"></param>
|
||||
/// <param name="allEntities"></param>
|
||||
/// <returns></returns>
|
||||
private static bool IsFiltered(
|
||||
IEntity entity,
|
||||
EventDefinitionTypeData eventDef,
|
||||
List<Tuple<IEntity, EventDefinitionTypeData>> allEntities)
|
||||
{
|
||||
var argType = eventDef.EventDefinition.Args.GetType();
|
||||
|
||||
//check if the entity is found in any processed event data that could possible supersede this one
|
||||
var foundByEntity = allEntities
|
||||
.Where(x => x.Item2.SupersedeAttributes.Length > 0
|
||||
//if it's the same arg type than it cannot supersede
|
||||
&& x.Item2.EventArgType != argType
|
||||
&& Equals(x.Item1, entity))
|
||||
.ToArray();
|
||||
|
||||
//no args have been processed with this entity so it should not be filtered
|
||||
if (foundByEntity.Length == 0)
|
||||
return false;
|
||||
|
||||
if (argType.IsGenericType)
|
||||
{
|
||||
var supercededBy = foundByEntity
|
||||
.FirstOrDefault(x =>
|
||||
x.Item2.SupersedeAttributes.Any(y =>
|
||||
//if the attribute type is a generic type def then compare with the generic type def of the event arg
|
||||
(y.SupersededEventArgsType.IsGenericTypeDefinition && y.SupersededEventArgsType == argType.GetGenericTypeDefinition())
|
||||
//if the attribute type is not a generic type def then compare with the normal type of the event arg
|
||||
|| (y.SupersededEventArgsType.IsGenericTypeDefinition == false && y.SupersededEventArgsType == argType)));
|
||||
return supercededBy != null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var supercededBy = foundByEntity
|
||||
.FirstOrDefault(x =>
|
||||
x.Item2.SupersedeAttributes.Any(y =>
|
||||
//since the event arg type is not a generic type, then we just compare type 1:1
|
||||
y.SupersededEventArgsType == argType));
|
||||
return supercededBy != null;
|
||||
}
|
||||
}
|
||||
|
||||
public void ScopeExit(bool completed)
|
||||
{
|
||||
if (_events == null) return;
|
||||
|
||||
20
src/Umbraco.Core/Events/SupersedeEventAttribute.cs
Normal file
20
src/Umbraco.Core/Events/SupersedeEventAttribute.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Core.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// This is used to know if the event arg attributed should supersede another event arg type when
|
||||
/// tracking events for the same entity. If one event args supercedes another then the event args that have been superseded
|
||||
/// will mean that the event will not be dispatched or the args will be filtered to exclude the entity.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
|
||||
internal class SupersedeEventAttribute : Attribute
|
||||
{
|
||||
public Type SupersededEventArgsType { get; private set; }
|
||||
|
||||
public SupersedeEventAttribute(Type supersededEventArgsType)
|
||||
{
|
||||
SupersededEventArgsType = supersededEventArgsType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,6 +339,7 @@
|
||||
<Compile Include="Events\IDeletingMediaFilesEventArgs.cs" />
|
||||
<Compile Include="Events\ScopeEventDispatcherBase.cs" />
|
||||
<Compile Include="Events\ScopeLifespanMessagesFactory.cs" />
|
||||
<Compile Include="Events\SupersedeEventAttribute.cs" />
|
||||
<Compile Include="Exceptions\ConnectionException.cs" />
|
||||
<Compile Include="IHttpContextAccessor.cs" />
|
||||
<Compile Include="Models\PublishedContent\PublishedContentTypeConverter.cs" />
|
||||
|
||||
@@ -92,6 +92,55 @@ namespace Umbraco.Tests.Scoping
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SupersededEvents()
|
||||
{
|
||||
DoSaveForContent += OnDoThingFail;
|
||||
DoDeleteForContent += OnDoThingFail;
|
||||
DoForTestArgs += OnDoThingFail;
|
||||
DoForTestArgs2 += OnDoThingFail;
|
||||
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
|
||||
var content1 = MockedContent.CreateBasicContent(contentType);
|
||||
content1.Id = 123;
|
||||
|
||||
var content2 = MockedContent.CreateBasicContent(contentType);
|
||||
content2.Id = 456;
|
||||
|
||||
var content3 = MockedContent.CreateBasicContent(contentType);
|
||||
content3.Id = 789;
|
||||
|
||||
var scopeProvider = new ScopeProvider(Mock.Of<IDatabaseFactory2>());
|
||||
using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher()))
|
||||
{
|
||||
|
||||
//content1 will be filtered from the args
|
||||
scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs<IContent>(new[]{ content1 , content3}));
|
||||
scope.Events.Dispatch(DoDeleteForContent, this, new DeleteEventArgs<IContent>(content1));
|
||||
scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs<IContent>(content2));
|
||||
//this entire event will be filtered
|
||||
scope.Events.Dispatch(DoForTestArgs, this, new TestEventArgs(content1));
|
||||
scope.Events.Dispatch(DoForTestArgs2, this, new TestEventArgs2(content1));
|
||||
|
||||
// events have been queued
|
||||
var events = scope.Events.GetEvents(EventDefinitionFilter.All).ToArray();
|
||||
Assert.AreEqual(4, events.Length);
|
||||
|
||||
Assert.AreEqual(typeof(SaveEventArgs<IContent>), events[0].Args.GetType());
|
||||
Assert.AreEqual(1, ((SaveEventArgs<IContent>)events[0].Args).SavedEntities.Count());
|
||||
Assert.AreEqual(content3.Id, ((SaveEventArgs<IContent>)events[0].Args).SavedEntities.First().Id);
|
||||
|
||||
Assert.AreEqual(typeof(DeleteEventArgs<IContent>), events[1].Args.GetType());
|
||||
Assert.AreEqual(content1.Id, ((DeleteEventArgs<IContent>) events[1].Args).DeletedEntities.First().Id);
|
||||
|
||||
Assert.AreEqual(typeof(SaveEventArgs<IContent>), events[2].Args.GetType());
|
||||
Assert.AreEqual(content2.Id, ((SaveEventArgs<IContent>)events[2].Args).SavedEntities.First().Id);
|
||||
|
||||
Assert.AreEqual(typeof(TestEventArgs2), events[3].Args.GetType());
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
@@ -100,7 +149,7 @@ namespace Umbraco.Tests.Scoping
|
||||
[Test]
|
||||
public void LatestEntities()
|
||||
{
|
||||
DoThingForContent += OnDoThingFail;
|
||||
DoSaveForContent += OnDoThingFail;
|
||||
|
||||
var now = DateTime.Now;
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
@@ -116,11 +165,10 @@ namespace Umbraco.Tests.Scoping
|
||||
|
||||
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));
|
||||
{
|
||||
scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs<IContent>(content1));
|
||||
scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs<IContent>(content2));
|
||||
scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs<IContent>(content3));
|
||||
|
||||
// events have been queued
|
||||
var events = scope.Events.GetEvents(EventDefinitionFilter.All).ToArray();
|
||||
@@ -225,14 +273,41 @@ namespace Umbraco.Tests.Scoping
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
public static event EventHandler<SaveEventArgs<IContent>> DoThingForContent;
|
||||
|
||||
public static event EventHandler<SaveEventArgs<IContent>> DoSaveForContent;
|
||||
public static event EventHandler<DeleteEventArgs<IContent>> DoDeleteForContent;
|
||||
public static event EventHandler<TestEventArgs> DoForTestArgs;
|
||||
public static event EventHandler<TestEventArgs2> DoForTestArgs2;
|
||||
public static event EventHandler<SaveEventArgs<string>> DoThing1;
|
||||
|
||||
public static event EventHandler<SaveEventArgs<int>> DoThing2;
|
||||
|
||||
public static event TypedEventHandler<ScopeEventDispatcherTests, SaveEventArgs<decimal>> DoThing3;
|
||||
|
||||
public class TestEventArgs : CancellableObjectEventArgs
|
||||
{
|
||||
public TestEventArgs(object eventObject) : base(eventObject)
|
||||
{
|
||||
}
|
||||
|
||||
public object MyEventObject
|
||||
{
|
||||
get { return EventObject; }
|
||||
}
|
||||
}
|
||||
|
||||
[SupersedeEvent(typeof(TestEventArgs))]
|
||||
public class TestEventArgs2 : CancellableObjectEventArgs
|
||||
{
|
||||
public TestEventArgs2(object eventObject) : base(eventObject)
|
||||
{
|
||||
}
|
||||
|
||||
public object MyEventObject
|
||||
{
|
||||
get { return EventObject; }
|
||||
}
|
||||
}
|
||||
|
||||
public class PassiveEventDispatcher : ScopeEventDispatcherBase
|
||||
{
|
||||
public PassiveEventDispatcher()
|
||||
|
||||
Reference in New Issue
Block a user