Files
Umbraco-CMS/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs

345 lines
16 KiB
C#
Raw Normal View History

2017-07-20 11:21:28 +02:00
using System;
2017-05-12 14:49:44 +02:00
using System.Collections.Generic;
using System.Linq;
using Umbraco.Cms.Core.Collections;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Extensions;
2017-05-12 14:49:44 +02:00
namespace Umbraco.Cms.Core.Events
2017-05-12 14:49:44 +02:00
{
2017-05-30 10:50:09 +02:00
/// <summary>
/// An IEventDispatcher that queues events.
/// </summary>
/// <remarks>
/// <para>Can raise, or ignore, cancelable events, depending on option.</para>
/// <para>Implementations must override ScopeExitCompleted to define what
/// to do with the events when the scope exits and has been completed.</para>
/// <para>If the scope exits without being completed, events are ignored.</para>
/// </remarks>
2017-05-12 14:49:44 +02:00
public abstract class QueuingEventDispatcherBase : IEventDispatcher
{
2017-05-30 10:50:09 +02:00
//events will be enlisted in the order they are raised
2017-05-12 14:49:44 +02:00
private List<IEventDefinition> _events;
private readonly bool _raiseCancelable;
protected QueuingEventDispatcherBase(bool raiseCancelable)
{
_raiseCancelable = raiseCancelable;
}
private List<IEventDefinition> Events => _events ?? (_events = new List<IEventDefinition>());
public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string eventName = null)
{
if (eventHandler == null) return args.Cancel;
if (_raiseCancelable == false) return args.Cancel;
eventHandler(sender, args);
return args.Cancel;
}
public bool DispatchCancelable<TArgs>(EventHandler<TArgs> eventHandler, object sender, TArgs args, string eventName = null)
where TArgs : CancellableEventArgs
{
if (eventHandler == null) return args.Cancel;
if (_raiseCancelable == false) return args.Cancel;
eventHandler(sender, args);
return args.Cancel;
}
public bool DispatchCancelable<TSender, TArgs>(TypedEventHandler<TSender, TArgs> eventHandler, TSender sender, TArgs args, string eventName = null)
where TArgs : CancellableEventArgs
{
if (eventHandler == null) return args.Cancel;
if (_raiseCancelable == false) return args.Cancel;
eventHandler(sender, args);
return args.Cancel;
}
public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string eventName = null)
{
if (eventHandler == null) return;
Events.Add(new EventDefinition(eventHandler, sender, args, eventName));
}
public void Dispatch<TArgs>(EventHandler<TArgs> eventHandler, object sender, TArgs args, string eventName = null)
{
if (eventHandler == null) return;
Events.Add(new EventDefinition<TArgs>(eventHandler, sender, args, eventName));
}
public void Dispatch<TSender, TArgs>(TypedEventHandler<TSender, TArgs> eventHandler, TSender sender, TArgs args, string eventName = null)
{
if (eventHandler == null) return;
Events.Add(new EventDefinition<TSender, TArgs>(eventHandler, sender, args, eventName));
}
public IEnumerable<IEventDefinition> GetEvents(EventDefinitionFilter filter)
{
if (_events == null)
return Enumerable.Empty<IEventDefinition>();
2018-03-21 09:06:32 +01:00
IReadOnlyList<IEventDefinition> events;
2017-05-12 14:49:44 +02:00
switch (filter)
{
case EventDefinitionFilter.All:
2018-03-21 09:06:32 +01:00
events = _events;
break;
2017-05-12 14:49:44 +02:00
case EventDefinitionFilter.FirstIn:
var l1 = new OrderedHashSet<IEventDefinition>();
foreach (var e in _events)
l1.Add(e);
2018-03-21 09:06:32 +01:00
events = l1;
break;
2017-05-12 14:49:44 +02:00
case EventDefinitionFilter.LastIn:
var l2 = new OrderedHashSet<IEventDefinition>(keepOldest: false);
foreach (var e in _events)
l2.Add(e);
2018-03-21 09:06:32 +01:00
events = l2;
break;
2017-05-12 14:49:44 +02:00
default:
2018-03-21 09:06:32 +01:00
throw new ArgumentOutOfRangeException("filter", filter, null);
2017-05-12 14:49:44 +02:00
}
2018-03-21 09:06:32 +01:00
return FilterSupersededAndUpdateToLatestEntity(events);
2017-05-12 14:49:44 +02:00
}
2018-03-21 09:06:32 +01:00
private class EventDefinitionInfos
2017-05-30 10:50:09 +02:00
{
public IEventDefinition EventDefinition { get; set; }
2018-03-21 09:06:32 +01:00
public Type[] SupersedeTypes { get; set; }
2017-05-30 10:50:09 +02:00
}
2019-01-22 18:03:39 -05:00
// this is way too convoluted, the supersede attribute is used only on DeleteEventargs to specify
// that it supersedes save, publish, move and copy - BUT - publish event args is also used for
// unpublishing and should NOT be superseded - so really it should not be managed at event args
2018-03-21 09:06:32 +01:00
// level but at event level
//
// what we want is:
// if an entity is deleted, then all Saved, Moved, Copied, Published events prior to this should
// not trigger for the entity - and even though, does it make any sense? making a copy of an entity
// should ... trigger?
//
// not going to refactor it all - we probably want to *always* trigger event but tell people that
// due to scopes, they should not expected eg a saved entity to still be around - however, now,
// going to write a ugly condition to deal with U4-10764
// iterates 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)
internal static IEnumerable<IEventDefinition> FilterSupersededAndUpdateToLatestEntity(IReadOnlyList<IEventDefinition> events)
2017-05-30 10:50:09 +02:00
{
2018-03-21 09:06:32 +01:00
// keeps the 'latest' entity and associated event data
var entities = new List<Tuple<IEntity, EventDefinitionInfos>>();
2017-05-30 10:50:09 +02:00
2018-03-21 09:06:32 +01:00
// collects the event definitions
// collects the arguments in result, that require their entities to be updated
2017-05-30 10:50:09 +02:00
var result = new List<IEventDefinition>();
2018-03-21 09:06:32 +01:00
var resultArgs = new List<CancellableObjectEventArgs>();
2017-05-30 10:50:09 +02:00
2019-01-22 18:03:39 -05:00
// eagerly fetch superseded arg types for each arg type
2018-03-21 09:06:32 +01:00
var argTypeSuperceeding = events.Select(x => x.Args.GetType())
2017-05-30 10:50:09 +02:00
.Distinct()
2018-03-21 09:06:32 +01:00
.ToDictionary(x => x, x => x.GetCustomAttributes<SupersedeEventAttribute>(false).Select(y => y.SupersededEventArgsType).ToArray());
2017-05-30 10:50:09 +02:00
2018-03-21 09:06:32 +01:00
// iterate over all events and filter
//
// process the list in reverse, because events are added in the order they are raised and we want to keep
// the latest (most recent) entities and filter out what is not relevant anymore (too old), eg if an entity
// is Deleted after being Saved, we want to filter out the Saved event
2017-05-30 10:50:09 +02:00
for (var index = events.Count - 1; index >= 0; index--)
{
2018-03-21 09:06:32 +01:00
var def = events[index];
2017-05-30 10:50:09 +02:00
2018-03-21 09:06:32 +01:00
var infos = new EventDefinitionInfos
2017-05-30 10:50:09 +02:00
{
2018-03-21 09:06:32 +01:00
EventDefinition = def,
SupersedeTypes = argTypeSuperceeding[def.Args.GetType()]
2017-05-30 10:50:09 +02:00
};
2018-03-21 09:06:32 +01:00
var args = def.Args as CancellableObjectEventArgs;
if (args == null)
2017-05-30 10:50:09 +02:00
{
2018-03-21 09:06:32 +01:00
// not a cancellable event arg, include event definition in result
result.Add(def);
}
else
{
// event object can either be a single object or an enumerable of objects
// try to get as an enumerable, get null if it's not
var eventObjects = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject);
if (eventObjects == null)
2017-05-30 10:50:09 +02:00
{
2018-03-21 09:06:32 +01:00
// single object, cast as an IEntity
// if cannot cast, cannot filter, nothing - just include event definition in result
var eventEntity = args.EventObject as IEntity;
if (eventEntity == null)
2017-05-30 10:50:09 +02:00
{
2018-03-21 09:06:32 +01:00
result.Add(def);
continue;
2017-05-30 10:50:09 +02:00
}
2018-03-21 09:06:32 +01:00
2019-01-22 18:03:39 -05:00
// look for this entity in superseding event args
2018-03-21 09:06:32 +01:00
// found = must be removed (ie not added), else track
if (IsSuperceeded(eventEntity, infos, entities) == false)
2017-05-30 10:50:09 +02:00
{
2018-03-21 09:06:32 +01:00
// track
entities.Add(Tuple.Create(eventEntity, infos));
// track result arguments
// include event definition in result
resultArgs.Add(args);
result.Add(def);
2017-05-30 10:50:09 +02:00
}
}
else
{
2018-03-21 09:06:32 +01:00
// enumerable of objects
2017-05-30 10:50:09 +02:00
var toRemove = new List<IEntity>();
2018-03-21 09:06:32 +01:00
foreach (var eventObject in eventObjects)
2017-05-30 10:50:09 +02:00
{
2018-03-21 09:06:32 +01:00
// extract the event object, cast as an IEntity
// if cannot cast, cannot filter, nothing to do - just leave it in the list & continue
var eventEntity = eventObject as IEntity;
if (eventEntity == null)
continue;
2019-01-22 18:03:39 -05:00
// look for this entity in superseding event args
2018-03-21 09:06:32 +01:00
// found = must be removed, else track
if (IsSuperceeded(eventEntity, infos, entities))
toRemove.Add(eventEntity);
2017-05-30 10:50:09 +02:00
else
2018-03-21 09:06:32 +01:00
entities.Add(Tuple.Create(eventEntity, infos));
2017-05-30 10:50:09 +02:00
}
2019-01-22 18:03:39 -05:00
// remove superseded entities
2017-05-30 10:50:09 +02:00
foreach (var entity in toRemove)
2018-03-21 09:06:32 +01:00
eventObjects.Remove(entity);
2017-05-30 10:50:09 +02:00
2018-03-21 09:06:32 +01:00
// if there are still entities in the list, keep the event definition
if (eventObjects.Count > 0)
2017-05-30 10:50:09 +02:00
{
if (toRemove.Count > 0)
{
2018-03-21 09:06:32 +01:00
// re-assign if changed
args.EventObject = eventObjects;
2017-05-30 10:50:09 +02:00
}
2018-03-21 09:06:32 +01:00
// track result arguments
// include event definition in result
resultArgs.Add(args);
result.Add(def);
2017-05-30 10:50:09 +02:00
}
}
}
}
2018-03-21 09:06:32 +01:00
// go over all args in result, and update them with the latest instanceof each entity
UpdateToLatestEntities(entities, resultArgs);
2017-05-30 10:50:09 +02:00
2018-03-21 09:06:32 +01:00
// reverse, since we processed the list in reverse
2017-05-30 10:50:09 +02:00
result.Reverse();
return result;
}
2018-03-21 09:06:32 +01:00
// edits event args to use the latest instance of each entity
private static void UpdateToLatestEntities(IEnumerable<Tuple<IEntity, EventDefinitionInfos>> entities, IEnumerable<CancellableObjectEventArgs> args)
2017-05-30 10:50:09 +02:00
{
2018-03-21 09:06:32 +01:00
// get the latest entities
// ordered hash set + keepOldest will keep the latest inserted entity (in case of duplicates)
2017-05-30 10:50:09 +02:00
var latestEntities = new OrderedHashSet<IEntity>(keepOldest: true);
2018-03-21 09:06:32 +01:00
foreach (var entity in entities.OrderByDescending(entity => entity.Item1.UpdateDate))
2017-05-30 10:50:09 +02:00
latestEntities.Add(entity.Item1);
2018-03-21 09:06:32 +01:00
foreach (var arg in args)
2017-05-30 10:50:09 +02:00
{
2018-03-21 09:06:32 +01:00
// event object can either be a single object or an enumerable of objects
// try to get as an enumerable, get null if it's not
var eventObjects = TypeHelper.CreateGenericEnumerableFromObject(arg.EventObject);
if (eventObjects == null)
2017-05-30 10:50:09 +02:00
{
2018-03-21 09:06:32 +01:00
// single object
// look for a more recent entity for that object, and replace if any
// works by "equalling" entities ie the more recent one "equals" this one (though different object)
var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, arg.EventObject));
2017-05-30 10:50:09 +02:00
if (foundEntity != null)
2018-03-21 09:06:32 +01:00
arg.EventObject = foundEntity;
2017-05-30 10:50:09 +02:00
}
else
{
2018-03-21 09:06:32 +01:00
// enumerable of objects
// same as above but for each object
2017-05-30 10:50:09 +02:00
var updated = false;
2018-03-21 09:06:32 +01:00
for (var i = 0; i < eventObjects.Count; i++)
2017-05-30 10:50:09 +02:00
{
2018-03-21 09:06:32 +01:00
var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, eventObjects[i]));
if (foundEntity == null) continue;
eventObjects[i] = foundEntity;
updated = true;
2017-05-30 10:50:09 +02:00
}
if (updated)
2018-03-21 09:06:32 +01:00
arg.EventObject = eventObjects;
2017-05-30 10:50:09 +02:00
}
}
}
2018-03-21 09:06:32 +01:00
// determines if a given entity, appearing in a given event definition, should be filtered out,
// considering the entities that have already been visited - an entity is filtered out if it
2019-01-22 18:03:39 -05:00
// appears in another even definition, which supersedes this event definition.
2018-03-21 09:06:32 +01:00
private static bool IsSuperceeded(IEntity entity, EventDefinitionInfos infos, List<Tuple<IEntity, EventDefinitionInfos>> entities)
2017-05-30 10:50:09 +02:00
{
2018-03-21 09:06:32 +01:00
//var argType = meta.EventArgsType;
var argType = infos.EventDefinition.Args.GetType();
2019-01-22 18:03:39 -05:00
// look for other instances of the same entity, coming from an event args that supersedes other event args,
2018-03-21 09:06:32 +01:00
// ie is marked with the attribute, and is not this event args (cannot supersede itself)
var superceeding = entities
.Where(x => x.Item2.SupersedeTypes.Length > 0 // has the attribute
&& x.Item2.EventDefinition.Args.GetType() != argType // is not the same
&& Equals(x.Item1, entity)) // same entity
2017-05-30 10:50:09 +02:00
.ToArray();
2018-03-21 09:06:32 +01:00
// first time we see this entity = not filtered
if (superceeding.Length == 0)
return false;
2019-01-22 18:03:39 -05:00
// delete event args does NOT supersedes 'unpublished' event
2018-10-03 14:27:48 +02:00
if (argType.IsGenericType && argType.GetGenericTypeDefinition() == typeof(PublishEventArgs<>) && infos.EventDefinition.EventName == "Unpublished")
2017-05-30 10:50:09 +02:00
return false;
2019-01-22 18:03:39 -05:00
// found occurrences, need to determine if this event args is superseded
2017-05-30 10:50:09 +02:00
if (argType.IsGenericType)
{
2018-03-21 09:06:32 +01:00
// generic, must compare type arguments
var supercededBy = superceeding.FirstOrDefault(x =>
x.Item2.SupersedeTypes.Any(y =>
2019-01-22 18:03:39 -05:00
// superseding a generic type which has the same generic type definition
2019-01-21 15:39:19 +01:00
// (but ... no matter the generic type parameters? could be different?)
2018-03-21 09:06:32 +01:00
y.IsGenericTypeDefinition && y == argType.GetGenericTypeDefinition()
2019-01-22 18:03:39 -05:00
// or superceeding a non-generic type which is ... (but... how is this ever possible? argType *is* generic?
2018-03-21 09:06:32 +01:00
|| y.IsGenericTypeDefinition == false && y == argType));
2017-05-30 10:50:09 +02:00
return supercededBy != null;
}
else
{
2018-03-21 09:06:32 +01:00
// non-generic, can compare types 1:1
var supercededBy = superceeding.FirstOrDefault(x =>
x.Item2.SupersedeTypes.Any(y => y == argType));
2017-05-30 10:50:09 +02:00
return supercededBy != null;
}
}
2017-05-12 14:49:44 +02:00
public void ScopeExit(bool completed)
{
if (_events == null) return;
if (completed)
ScopeExitCompleted();
_events.Clear();
}
protected abstract void ScopeExitCompleted();
}
2017-07-20 11:21:28 +02:00
}