-
Notifications
You must be signed in to change notification settings - Fork 2.6k
/
QueuingEventDispatcherBase.cs
344 lines (305 loc) · 16 KB
/
QueuingEventDispatcherBase.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
using System;
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;
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
{
//events will be enlisted in the order they are raised
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>();
IReadOnlyList<IEventDefinition> events;
switch (filter)
{
case EventDefinitionFilter.All:
events = _events;
break;
case EventDefinitionFilter.FirstIn:
var l1 = new OrderedHashSet<IEventDefinition>();
foreach (var e in _events)
l1.Add(e);
events = l1;
break;
case EventDefinitionFilter.LastIn:
var l2 = new OrderedHashSet<IEventDefinition>(keepOldest: false);
foreach (var e in _events)
l2.Add(e);
events = l2;
break;
default:
throw new ArgumentOutOfRangeException("filter", filter, null);
}
return FilterSupersededAndUpdateToLatestEntity(events);
}
private class EventDefinitionInfos
{
public IEventDefinition? EventDefinition { get; set; }
public Type[]? SupersedeTypes { get; set; }
}
// 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--)
{
var 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
var 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
var eventEntity = args.EventObject as IEntity;
if (eventEntity == null)
{
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
var eventEntity = eventObject as IEntity;
if (eventEntity == null)
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 (var 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;
}
// 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>(keepOldest: true);
foreach (var entity in entities.OrderByDescending(entity => entity.Item1.UpdateDate))
latestEntities.Add(entity.Item1);
foreach (var 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
var 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)
var 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++)
{
var 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;
var 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)
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
.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
var 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
var supercededBy = superceeding.FirstOrDefault(x =>
x.Item2.SupersedeTypes?.Any(y => y == argType) ?? false);
return supercededBy != null;
}
}
public void ScopeExit(bool completed)
{
if (_events == null) return;
if (completed)
ScopeExitCompleted();
_events.Clear();
}
protected abstract void ScopeExitCompleted();
}
}