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

424 lines
16 KiB
C#

using System.Collections;
using Umbraco.Cms.Core.Collections;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Events;
/// <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>
public abstract class QueuingEventDispatcherBase : IEventDispatcher
{
private readonly bool _raiseCancelable;
// events will be enlisted in the order they are raised
private List<IEventDefinition>? _events;
protected QueuingEventDispatcherBase(bool raiseCancelable) => _raiseCancelable = raiseCancelable;
private List<IEventDefinition> 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>();
}
IReadOnlyList<IEventDefinition> events;
switch (filter)
{
case EventDefinitionFilter.All:
events = _events;
break;
case EventDefinitionFilter.FirstIn:
var l1 = new OrderedHashSet<IEventDefinition>();
foreach (IEventDefinition e in _events)
{
l1.Add(e);
}
events = l1;
break;
case EventDefinitionFilter.LastIn:
var l2 = new OrderedHashSet<IEventDefinition>(false);
foreach (IEventDefinition e in _events)
{
l2.Add(e);
}
events = l2;
break;
default:
throw new ArgumentOutOfRangeException("filter", filter, null);
}
return FilterSupersededAndUpdateToLatestEntity(events);
}
public void ScopeExit(bool completed)
{
if (_events == null)
{
return;
}
if (completed)
{
ScopeExitCompleted();
}
_events.Clear();
}
// 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
// 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)
{
// keeps the 'latest' entity and associated event data
var entities = new List<Tuple<IEntity, EventDefinitionInfos>>();
// collects the event definitions
// collects the arguments in result, that require their entities to be updated
var result = new List<IEventDefinition>();
var resultArgs = new List<CancellableObjectEventArgs>();
// eagerly fetch superseded arg types for each arg type
var argTypeSuperceeding = events.Select(x => x.Args.GetType())
.Distinct()
.ToDictionary(
x => x,
x => x.GetCustomAttributes<SupersedeEventAttribute>(false).Select(y => y.SupersededEventArgsType)
.ToArray());
// 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
for (var index = events.Count - 1; index >= 0; index--)
{
IEventDefinition def = events[index];
var infos = new EventDefinitionInfos
{
EventDefinition = def,
SupersedeTypes = argTypeSuperceeding[def.Args.GetType()],
};
var args = def.Args as CancellableObjectEventArgs;
if (args == null)
{
// 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
IList? eventObjects = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject);
if (eventObjects == null)
{
// single object, cast as an IEntity
// if cannot cast, cannot filter, nothing - just include event definition in result
if (args.EventObject is not IEntity eventEntity)
{
result.Add(def);
continue;
}
// look for this entity in superseding event args
// found = must be removed (ie not added), else track
if (IsSuperceeded(eventEntity, infos, entities) == false)
{
// track
entities.Add(Tuple.Create(eventEntity, infos));
// track result arguments
// include event definition in result
resultArgs.Add(args);
result.Add(def);
}
}
else
{
// enumerable of objects
var toRemove = new List<IEntity>();
foreach (var eventObject in eventObjects)
{
// extract the event object, cast as an IEntity
// if cannot cast, cannot filter, nothing to do - just leave it in the list & continue
if (eventObject is not IEntity eventEntity)
{
continue;
}
// look for this entity in superseding event args
// found = must be removed, else track
if (IsSuperceeded(eventEntity, infos, entities))
{
toRemove.Add(eventEntity);
}
else
{
entities.Add(Tuple.Create(eventEntity, infos));
}
}
// remove superseded entities
foreach (IEntity entity in toRemove)
{
eventObjects.Remove(entity);
}
// if there are still entities in the list, keep the event definition
if (eventObjects.Count > 0)
{
if (toRemove.Count > 0)
{
// re-assign if changed
args.EventObject = eventObjects;
}
// track result arguments
// include event definition in result
resultArgs.Add(args);
result.Add(def);
}
}
}
}
// go over all args in result, and update them with the latest instanceof each entity
UpdateToLatestEntities(entities, resultArgs);
// reverse, since we processed the list in reverse
result.Reverse();
return result;
}
protected abstract void ScopeExitCompleted();
// edits event args to use the latest instance of each entity
private static void UpdateToLatestEntities(
IEnumerable<Tuple<IEntity, EventDefinitionInfos>> entities,
IEnumerable<CancellableObjectEventArgs> args)
{
// get the latest entities
// ordered hash set + keepOldest will keep the latest inserted entity (in case of duplicates)
var latestEntities = new OrderedHashSet<IEntity>(true);
foreach (Tuple<IEntity, EventDefinitionInfos> entity in entities.OrderByDescending(entity =>
entity.Item1.UpdateDate))
{
latestEntities.Add(entity.Item1);
}
foreach (CancellableObjectEventArgs arg in args)
{
// 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
IList? eventObjects = TypeHelper.CreateGenericEnumerableFromObject(arg.EventObject);
if (eventObjects == null)
{
// 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)
IEntity? foundEntity = latestEntities.FirstOrDefault(x => Equals(x, arg.EventObject));
if (foundEntity != null)
{
arg.EventObject = foundEntity;
}
}
else
{
// enumerable of objects
// same as above but for each object
var updated = false;
for (var i = 0; i < eventObjects.Count; i++)
{
IEntity? foundEntity = latestEntities.FirstOrDefault(x => Equals(x, eventObjects[i]));
if (foundEntity == null)
{
continue;
}
eventObjects[i] = foundEntity;
updated = true;
}
if (updated)
{
arg.EventObject = eventObjects;
}
}
}
}
// 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
// appears in another even definition, which supersedes this event definition.
private static bool IsSuperceeded(IEntity entity, EventDefinitionInfos infos, List<Tuple<IEntity, EventDefinitionInfos>> entities)
{
// var argType = meta.EventArgsType;
Type? argType = infos.EventDefinition?.Args.GetType();
// look for other instances of the same entity, coming from an event args that supersedes other event args,
// ie is marked with the attribute, and is not this event args (cannot supersede itself)
Tuple<IEntity, EventDefinitionInfos>[] 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
.ToArray();
// first time we see this entity = not filtered
if (superceeding.Length == 0)
{
return false;
}
// delete event args does NOT supersedes 'unpublished' event
if ((argType?.IsGenericType ?? false) && argType.GetGenericTypeDefinition() == typeof(PublishEventArgs<>) &&
infos.EventDefinition?.EventName == "Unpublished")
{
return false;
}
// found occurrences, need to determine if this event args is superseded
if (argType?.IsGenericType ?? false)
{
// generic, must compare type arguments
Tuple<IEntity, EventDefinitionInfos>? supercededBy = superceeding.FirstOrDefault(x =>
x.Item2.SupersedeTypes?.Any(y =>
// superseding a generic type which has the same generic type definition
// (but ... no matter the generic type parameters? could be different?)
(y.IsGenericTypeDefinition && y == argType.GetGenericTypeDefinition())
// or superceeding a non-generic type which is ... (but... how is this ever possible? argType *is* generic?
|| (y.IsGenericTypeDefinition == false && y == argType)) ?? false);
return supercededBy != null;
}
else
{
// non-generic, can compare types 1:1
Tuple<IEntity, EventDefinitionInfos>? supercededBy = superceeding.FirstOrDefault(x =>
x.Item2.SupersedeTypes?.Any(y => y == argType) ?? false);
return supercededBy != null;
}
}
private sealed class EventDefinitionInfos
{
public IEventDefinition? EventDefinition { get; set; }
public Type[]? SupersedeTypes { get; set; }
}
}