-
Notifications
You must be signed in to change notification settings - Fork 387
Expand file tree
/
Copy pathStrongReferenceMessenger.cs
More file actions
871 lines (757 loc) · 40.3 KB
/
StrongReferenceMessenger.cs
File metadata and controls
871 lines (757 loc) · 40.3 KB
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
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Threading;
using CommunityToolkit.Mvvm.Messaging.Internals;
namespace CommunityToolkit.Mvvm.Messaging;
/// <summary>
/// A class providing a reference implementation for the <see cref="IMessenger"/> interface.
/// </summary>
/// <remarks>
/// This <see cref="IMessenger"/> implementation uses strong references to track the registered
/// recipients, so it is necessary to manually unregister them when they're no longer needed.
/// </remarks>
public sealed class StrongReferenceMessenger : IMessenger
{
// This messenger uses the following logic to link stored instances together:
// --------------------------------------------------------------------------------------------------------
// Dictionary2<Recipient, HashSet<IMapping>> recipientsMap;
// | \________________[*]IDictionary2<Recipient, IDictionary2<TToken>>
// | \_______________[*]IDictionary2<Recipient, object?> /
// | \_________/_________/___ /
// |\ _(recipients registrations)_/ / \ /
// | \__________________ / _____(channel registrations)_____/______\____/
// | \ / / __________________________/ \
// | / / / \
// | Dictionary2<Recipient, object?> mapping = Mapping________________\
// | __________________/ / | / \
// |/ / | / \
// Dictionary2<Recipient, Dictionary2<TToken, object?>> mapping = Mapping<TToken>____________\
// / / / /
// ___(Type2.TToken)____/ / / /
// /________________(Type2.TMessage)_______/_______/__/
// / ________________________________/
// / /
// Dictionary2<Type2, IMapping> typesMap;
// --------------------------------------------------------------------------------------------------------
// Each combination of <TMessage, TToken> results in a concrete Mapping type (if TToken is Unit) or Mapping<Token> type,
// which holds the references from registered recipients to handlers. Mapping is used when the default channel is being
// requested, as in that case there will only ever be up to a handler per recipient, per message type. In that case,
// each recipient will only track the message dispatcher (stored as an object?, see notes below), instead of a dictionary
// mapping each TToken value to the corresponding dispatcher for that recipient. When a custom channel is used, the
// dispatchers are stored in a <TToken, object?> dictionary, so that each recipient can have up to one registered handler
// for a given token, for each message type. Note that the registered dispatchers are only stored as object references, as
// they can either be null or a MessageHandlerDispatcher.For<TRecipient, TMessage> instance.
//
// The first case happens if the handler was registered through an IRecipient<TMessage> instance, while the second one is
// used to wrap input MessageHandler<TRecipient, TMessage> instances. The MessageHandlerDispatcher.For<TRecipient, TMessage>
// instances will just be cast to MessageHandlerDispatcher when invoking it. This allows users to retain type information on
// each registered recipient, instead of having to manually cast each recipient to the right type within the handler
// (additionally, using double dispatch here avoids the need to alias delegate types). The type conversion is guaranteed to be
// respected due to how the messenger type itself works - as registered handlers are always invoked on their respective recipients.
//
// Each mapping is stored in the types map, which associates each pair of concrete types to its mapping instance. Mapping instances
// are exposed as IMapping items, as each will be a closed type over a different combination of TMessage and TToken generic type
// parameters (or just of TMessage, for the default channel). Each existing recipient is also stored in the main recipients map,
// along with a set of all the existing (dictionaries of) handlers for that recipient (for all message types and token types, if any).
//
// A recipient is stored in the main map as long as it has at least one registered handler in any of the existing mappings for every
// message/token type combination. The shared map is used to access the set of all registered handlers for a given recipient, without
// having to know in advance the type of message or token being used for the registration, and without having to use reflection. This
// is the same approach used in the types map, as we expose saved items as IMapping values too.
//
// Note that each mapping stored in the associated set for each recipient also indirectly implements either IDictionary2<Recipient, Token>
// or IDictionary2<Recipient>, with any token type currently in use by that recipient (or none, if using the default channel). This allows
// to retrieve the type-closed mappings of registered handlers with a given token type, for any message type, for every receiver, again
// without having to use reflection. This shared map is used to unregister messages from a given recipients either unconditionally, by
// message type, by token, or for a specific pair of message type and token value.
/// <summary>
/// The collection of currently registered recipients, with a link to their linked message receivers.
/// </summary>
/// <remarks>
/// This collection is used to allow reflection-free access to all the existing
/// registered recipients from <see cref="UnregisterAll"/> and other methods in this type,
/// so that all the existing handlers can be removed without having to dynamically create
/// the generic types for the containers of the various dictionaries mapping the handlers.
/// </remarks>
private readonly Dictionary2<Recipient, HashSet<IMapping>> recipientsMap = new();
/// <summary>
/// The <see cref="Mapping"/> and <see cref="Mapping{TToken}"/> instance for types combination.
/// </summary>
/// <remarks>
/// The values are just of type <see cref="IDictionary2{T}"/> as we don't know the type parameters in advance.
/// Each method relies on <see cref="GetOrAddMapping{TMessage,TToken}"/> to get the type-safe instance of the
/// <see cref="Mapping"/> or <see cref="Mapping{TToken}"/> class for each pair of generic arguments in use.
/// </remarks>
private readonly Dictionary2<Type2, IMapping> typesMap = new();
#if NET9_0_OR_GREATER
/// <summary>
/// The <see cref="Lock"/> used to synchronize access to <see cref="recipientsMap"/>.
/// </summary>
private readonly Lock recipientsMapLock = new();
#else
/// <summary>
/// The lock object used to synchronize access to <see cref="recipientsMap"/>.
/// </summary>
private readonly object recipientsMapLock = new();
#endif
/// <summary>
/// Gets the default <see cref="StrongReferenceMessenger"/> instance.
/// </summary>
public static StrongReferenceMessenger Default { get; } = new();
/// <inheritdoc/>
public bool IsRegistered<TMessage, TToken>(object recipient, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
ArgumentNullException.ThrowIfNull(recipient);
ArgumentNullException.For<TToken>.ThrowIfNull(token);
lock (this.recipientsMapLock)
{
if (typeof(TToken) == typeof(Unit))
{
if (!TryGetMapping<TMessage>(out Mapping? mapping))
{
return false;
}
Recipient key = new(recipient);
return mapping.ContainsKey(key);
}
else
{
if (!TryGetMapping<TMessage, TToken>(out Mapping<TToken>? mapping))
{
return false;
}
Recipient key = new(recipient);
return
mapping.TryGetValue(key, out Dictionary2<TToken, object?>? handlers) &&
handlers.ContainsKey(token);
}
}
}
/// <inheritdoc/>
public void Register<TRecipient, TMessage, TToken>(TRecipient recipient, TToken token, MessageHandler<TRecipient, TMessage> handler)
where TRecipient : class
where TMessage : class
where TToken : IEquatable<TToken>
{
ArgumentNullException.ThrowIfNull(recipient);
ArgumentNullException.For<TToken>.ThrowIfNull(token);
ArgumentNullException.ThrowIfNull(handler);
Register<TMessage, TToken>(recipient, token, new MessageHandlerDispatcher.For<TRecipient, TMessage>(handler));
}
/// <inheritdoc cref="WeakReferenceMessenger.Register{TMessage, TToken}(IRecipient{TMessage}, TToken)"/>
internal void Register<TMessage, TToken>(IRecipient<TMessage> recipient, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
Register<TMessage, TToken>(recipient, token, null);
}
/// <summary>
/// Registers a recipient for a given type of message.
/// </summary>
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
/// <typeparam name="TToken">The type of token to use to pick the messages to receive.</typeparam>
/// <param name="recipient">The recipient that will receive the messages.</param>
/// <param name="token">A token used to determine the receiving channel to use.</param>
/// <param name="dispatcher">The input <see cref="MessageHandlerDispatcher"/> instance to register, or null.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
private void Register<TMessage, TToken>(object recipient, TToken token, MessageHandlerDispatcher? dispatcher)
where TMessage : class
where TToken : IEquatable<TToken>
{
lock (this.recipientsMapLock)
{
Recipient key = new(recipient);
IMapping mapping;
// Fast path for unit tokens
if (typeof(TToken) == typeof(Unit))
{
// Get the <TMessage> registration list for this recipient
Mapping underlyingMapping = GetOrAddMapping<TMessage>();
ref object? registeredHandler = ref underlyingMapping.GetOrAddValueRef(key);
if (registeredHandler is not null)
{
ThrowInvalidOperationExceptionForDuplicateRegistration();
}
// Store the input handler
registeredHandler = dispatcher;
mapping = underlyingMapping;
}
else
{
// Get the <TMessage, TToken> registration list for this recipient
Mapping<TToken> underlyingMapping = GetOrAddMapping<TMessage, TToken>();
ref Dictionary2<TToken, object?>? map = ref underlyingMapping.GetOrAddValueRef(key);
map ??= new Dictionary2<TToken, object?>();
// Add the new registration entry
ref object? registeredHandler = ref map.GetOrAddValueRef(token);
if (registeredHandler is not null)
{
ThrowInvalidOperationExceptionForDuplicateRegistration();
}
registeredHandler = dispatcher;
mapping = underlyingMapping;
}
// Make sure this registration map is tracked for the current recipient
ref HashSet<IMapping>? set = ref this.recipientsMap.GetOrAddValueRef(key);
set ??= new HashSet<IMapping>();
_ = set.Add(mapping);
}
}
/// <inheritdoc/>
public void UnregisterAll(object recipient)
{
ArgumentNullException.ThrowIfNull(recipient);
lock (this.recipientsMapLock)
{
// If the recipient has no registered messages at all, ignore
Recipient key = new(recipient);
if (!this.recipientsMap.TryGetValue(key, out HashSet<IMapping>? set))
{
return;
}
// Removes all the lists of registered handlers for the recipient
foreach (IMapping mapping in set)
{
if (mapping.TryRemove(key) &&
mapping.Count == 0)
{
// Maps here are really of type Mapping<,> and with unknown type arguments.
// If after removing the current recipient a given map becomes empty, it means
// that there are no registered recipients at all for a given pair of message
// and token types. In that case, we also remove the map from the types map.
// The reason for keeping a key in each mapping is that removing items from a
// dictionary (a hashed collection) only costs O(1) in the best case, while
// if we had tried to iterate the whole dictionary every time we would have
// paid an O(n) minimum cost for each single remove operation.
_ = this.typesMap.TryRemove(mapping.TypeArguments);
}
}
// Remove the associated set in the recipients map
_ = this.recipientsMap.TryRemove(key);
}
}
/// <inheritdoc/>
public void UnregisterAll<TToken>(object recipient, TToken token)
where TToken : IEquatable<TToken>
{
ArgumentNullException.ThrowIfNull(recipient);
ArgumentNullException.For<TToken>.ThrowIfNull(token);
// This method is never called with the unit type, so this path is not implemented. This
// exception should not ever be thrown, it's here just to double check for regressions in
// case a bug was introduced that caused this path to somehow be invoked with the Unit type.
// This type is internal, so consumers of the library would never be able to pass it here,
// and there are (and shouldn't be) any APIs publicly exposed from the library that would
// cause this path to be taken either. When using the default channel, only UnregisterAll(object)
// is supported, which would just unregister all recipients regardless of the selected channel.
if (typeof(TToken) == typeof(Unit))
{
throw new NotImplementedException();
}
bool lockTaken = false;
object[]? maps = null;
int i = 0;
// We use an explicit try/finally block here instead of the lock syntax so that we can use a single
// one both to release the lock and to clear the rented buffer and return it to the pool. The reason
// why we're declaring the buffer here and clearing and returning it in this outer finally block is
// that doing so doesn't require the lock to be kept, and releasing it before performing this last
// step reduces the total time spent while the lock is acquired, which in turn reduces the lock
// contention in multi-threaded scenarios where this method is invoked concurrently.
try
{
Monitor.Enter(this.recipientsMap, ref lockTaken);
// Get the shared set of mappings for the recipient, if present
Recipient key = new(recipient);
if (!this.recipientsMap.TryGetValue(key, out HashSet<IMapping>? set))
{
return;
}
// Copy the candidate mappings for the target recipient to a local array, as we can't modify the
// contents of the set while iterating it. The rented buffer is oversized and will also include
// mappings for handlers of messages that are registered through a different token. Note that
// we're using just an object array to minimize the number of total rented buffers, that would
// just remain in the shared pool unused, other than when they are rented here. Instead, we're
// using a type that would possibly also be used by the users of the library, which increases
// the opportunities to reuse existing buffers for both. When we need to reference an item
// stored in the buffer with the type we know it will have, we use Unsafe.As<T> to avoid the
// expensive type check in the cast, since we already know the assignment will be valid.
maps = ArrayPool<object>.Shared.Rent(set.Count);
foreach (IMapping item in set)
{
// Select all mappings using the same token type
if (item is IDictionary2<Recipient, IDictionary2<TToken>> mapping)
{
maps[i++] = mapping;
}
}
// Iterate through all the local maps. These are all the currently
// existing maps of handlers for messages of any given type, with a token
// of the current type, for the target recipient. We heavily rely on
// interfaces here to be able to iterate through all the available mappings
// without having to know the concrete type in advance, and without having
// to deal with reflection: we can just check if the type of the closed interface
// matches with the token type currently in use, and operate on those instances.
foreach (object obj in maps.AsSpan(0, i))
{
IDictionary2<Recipient, IDictionary2<TToken>>? handlersMap = Unsafe.As<IDictionary2<Recipient, IDictionary2<TToken>>>(obj);
// We don't need whether or not the map contains the recipient, as the
// sequence of maps has already been copied from the set containing all
// the mappings for the target recipients: it is guaranteed to be here.
IDictionary2<TToken> holder = handlersMap[key];
// Try to remove the registered handler for the input token,
// for the current message type (unknown from here).
if (holder.TryRemove(token) &&
holder.Count == 0)
{
// If the map is empty, remove the recipient entirely from its container
_ = handlersMap.TryRemove(key);
IMapping mapping = Unsafe.As<IMapping>(handlersMap);
// This recipient has no registrations left for this combination of token
// and message type, so this mapping can be removed from its associated set.
_ = set.Remove(mapping);
// If the resulting set is empty, then this means that there are no more handlers
// left for this recipient for any message or token type, so the recipient can also
// be removed from the map of all existing recipients with at least one handler.
if (set.Count == 0)
{
_ = this.recipientsMap.TryRemove(key);
}
// If no handlers are left at all for any recipient, across all message types and token
// types, remove the set of mappings entirely for the current recipient, and remove the
// strong reference to it as well. This is the same situation that would've been achieved
// by just calling UnregisterAll(recipient).
if (handlersMap.Count == 0)
{
_ = this.typesMap.TryRemove(mapping.TypeArguments);
}
}
}
}
finally
{
// Release the lock, if we did acquire it
if (lockTaken)
{
Monitor.Exit(this.recipientsMap);
}
// If we got to renting the array of maps, return it to the shared pool.
// Remove references to avoid leaks coming from the shared memory pool.
// We manually create a span and clear it as a small optimization, as
// arrays rented from the pool can be larger than the requested size.
if (maps is not null)
{
maps.AsSpan(0, i).Clear();
ArrayPool<object>.Shared.Return(maps);
}
}
}
/// <inheritdoc/>
public void Unregister<TMessage, TToken>(object recipient, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
ArgumentNullException.ThrowIfNull(recipient);
ArgumentNullException.For<TToken>.ThrowIfNull(token);
lock (this.recipientsMapLock)
{
if (typeof(TToken) == typeof(Unit))
{
// Get the registration list, if available
if (!TryGetMapping<TMessage>(out Mapping? mapping))
{
return;
}
Recipient key = new(recipient);
// Remove the handler (there can only be one for the unit type)
if (!mapping.TryRemove(key))
{
return;
}
// Remove the map entirely from this container, and remove the link to the map itself to
// the current mapping between existing registered recipients (or entire recipients too).
// This is the same as below, except for the unit type there can only be one handler, so
// removing it already implies the target recipient has no remaining handlers left.
_ = mapping.TryRemove(key);
// If there are no handlers left at all for this type combination, drop it
if (mapping.Count == 0)
{
_ = this.typesMap.TryRemove(mapping.TypeArguments);
}
HashSet<IMapping> set = this.recipientsMap[key];
// The current mapping no longer has any handlers left for this recipient.
// Remove it and then also remove the recipient if this was the last handler.
// Again, this is the same as below, except with the assumption of the unit type.
_ = set.Remove(mapping);
if (set.Count == 0)
{
_ = this.recipientsMap.TryRemove(key);
}
}
else
{
// Get the registration list, if available
if (!TryGetMapping<TMessage, TToken>(out Mapping<TToken>? mapping))
{
return;
}
Recipient key = new(recipient);
if (!mapping.TryGetValue(key, out Dictionary2<TToken, object?>? dictionary))
{
return;
}
// Remove the target handler
if (dictionary.TryRemove(token) &&
dictionary.Count == 0)
{
// If the map is empty, it means that the current recipient has no remaining
// registered handlers for the current <TMessage, TToken> combination, regardless,
// of the specific token value (ie. the channel used to receive messages of that type).
// We can remove the map entirely from this container, and remove the link to the map itself
// to the current mapping between existing registered recipients (or entire recipients too).
_ = mapping.TryRemove(key);
// If there are no handlers left at all for this type combination, drop it
if (mapping.Count == 0)
{
_ = this.typesMap.TryRemove(mapping.TypeArguments);
}
HashSet<IMapping> set = this.recipientsMap[key];
// The current mapping no longer has any handlers left for this recipient
_ = set.Remove(mapping);
// If the current recipients has no handlers left at all, remove it
if (set.Count == 0)
{
_ = this.recipientsMap.TryRemove(key);
}
}
}
}
}
/// <inheritdoc/>
public TMessage Send<TMessage, TToken>(TMessage message, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
ArgumentNullException.ThrowIfNull(message);
ArgumentNullException.For<TToken>.ThrowIfNull(token);
object?[] rentedArray;
Span<object?> pairs;
int i = 0;
lock (this.recipientsMapLock)
{
if (typeof(TToken) == typeof(Unit))
{
// Check whether there are any registered recipients
if (!TryGetMapping<TMessage>(out Mapping? mapping))
{
goto End;
}
// Check the number of remaining handlers, see below
int totalHandlersCount = mapping.Count;
if (totalHandlersCount == 0)
{
goto End;
}
pairs = rentedArray = ArrayPool<object?>.Shared.Rent(2 * totalHandlersCount);
// Same logic as below, except here we're only traversing one handler per recipient
Dictionary2<Recipient, object?>.Enumerator mappingEnumerator = mapping.GetEnumerator();
while (mappingEnumerator.MoveNext())
{
pairs[2 * i] = mappingEnumerator.GetValue();
pairs[(2 * i) + 1] = mappingEnumerator.GetKey().Target;
i++;
}
}
else
{
// Check whether there are any registered recipients
if (!TryGetMapping<TMessage, TToken>(out Mapping<TToken>? mapping))
{
goto End;
}
// We need to make a local copy of the currently registered handlers, since users might
// try to unregister (or register) new handlers from inside one of the currently existing
// handlers. We can use memory pooling to reuse arrays, to minimize the average memory
// usage. In practice, we usually just need to pay the small overhead of copying the items.
// The current mapping contains all the currently registered recipients and handlers for
// the <TMessage, TToken> combination in use. In the worst case scenario, all recipients
// will have a registered handler with a token matching the input one, meaning that we could
// have at worst a number of pending handlers to invoke equal to the total number of recipient
// in the mapping. This relies on the fact that tokens are unique, and that there is only
// one handler associated with a given token. We can use this upper bound as the requested
// size for each array rented from the pool, which guarantees that we'll have enough space.
int totalHandlersCount = mapping.Count;
if (totalHandlersCount == 0)
{
goto End;
}
// Rent the array and also assign it to a span, which will be used to access values.
// We're doing this to avoid the array covariance checks slowdown in the loops below.
pairs = rentedArray = ArrayPool<object?>.Shared.Rent(2 * totalHandlersCount);
// Copy the handlers to the local collection.
// The array is oversized at this point, since it also includes
// handlers for different tokens. We can reuse the same variable
// to count the number of matching handlers to invoke later on.
// This will be the array slice with valid handler in the rented buffer.
Dictionary2<Recipient, Dictionary2<TToken, object?>>.Enumerator mappingEnumerator = mapping.GetEnumerator();
// Explicit enumerator usage here as we're using a custom one
// that doesn't expose the single standard Current property.
while (mappingEnumerator.MoveNext())
{
// Pick the target handler, if the token is a match for the recipient
if (mappingEnumerator.GetValue().TryGetValue(token, out object? handler))
{
// This span access should always guaranteed to be valid due to the size of the
// array being set according to the current total number of registered handlers,
// which will always be greater or equal than the ones matching the previous test.
// We're still using a checked span accesses here though to make sure an out of
// bounds write can never happen even if an error was present in the logic above.
pairs[2 * i] = handler;
pairs[(2 * i) + 1] = mappingEnumerator.GetKey().Target;
i++;
}
}
}
}
try
{
// The core broadcasting logic is the same as the weak reference messenger one
WeakReferenceMessenger.SendAll(pairs, i, message);
}
finally
{
// As before, we also need to clear it first to avoid having potentially long
// lasting memory leaks due to leftover references being stored in the pool.
Array.Clear(rentedArray, 0, 2 * i);
ArrayPool<object?>.Shared.Return(rentedArray);
}
End:
return message;
}
/// <inheritdoc/>
void IMessenger.Cleanup()
{
// The current implementation doesn't require any kind of cleanup operation, as
// all the internal data structures are already kept in sync whenever a recipient
// is added or removed. This method is implemented through an explicit interface
// implementation so that developers using this type directly will not see it in
// the API surface (as it wouldn't be useful anyway, since it's a no-op here).
}
/// <inheritdoc/>
public void Reset()
{
lock (this.recipientsMapLock)
{
this.recipientsMap.Clear();
this.typesMap.Clear();
}
}
/// <summary>
/// Tries to get the <see cref="Mapping"/> instance of currently
/// registered recipients for the input <typeparamref name="TMessage"/> type.
/// </summary>
/// <typeparam name="TMessage">The type of message to send.</typeparam>
/// <param name="mapping">The resulting <see cref="Mapping"/> instance, if found.</param>
/// <returns>Whether or not the required <see cref="Mapping"/> instance was found.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryGetMapping<TMessage>([NotNullWhen(true)] out Mapping? mapping)
where TMessage : class
{
Type2 key = new(typeof(TMessage), typeof(Unit));
if (this.typesMap.TryGetValue(key, out IMapping? target))
{
// This method and the ones below are the only ones handling values in the types map,
// and here we are sure that the object reference we have points to an instance of the
// right type. Using an unsafe cast skips two conditional branches and is faster.
mapping = Unsafe.As<Mapping>(target);
return true;
}
mapping = null;
return false;
}
/// <summary>
/// Tries to get the <see cref="Mapping{TToken}"/> instance of currently registered recipients
/// for the combination of types <typeparamref name="TMessage"/> and <typeparamref name="TToken"/>.
/// </summary>
/// <typeparam name="TMessage">The type of message to send.</typeparam>
/// <typeparam name="TToken">The type of token to identify what channel to use to send the message.</typeparam>
/// <param name="mapping">The resulting <see cref="Mapping{TToken}"/> instance, if found.</param>
/// <returns>Whether or not the required <see cref="Mapping{TToken}"/> instance was found.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryGetMapping<TMessage, TToken>([NotNullWhen(true)] out Mapping<TToken>? mapping)
where TMessage : class
where TToken : IEquatable<TToken>
{
Type2 key = new(typeof(TMessage), typeof(TToken));
if (this.typesMap.TryGetValue(key, out IMapping? target))
{
mapping = Unsafe.As<Mapping<TToken>>(target);
return true;
}
mapping = null;
return false;
}
/// <summary>
/// Gets the <see cref="Mapping"/> instance of currently
/// registered recipients for the input <typeparamref name="TMessage"/> type.
/// </summary>
/// <typeparam name="TMessage">The type of message to send.</typeparam>
/// <returns>A <see cref="Mapping"/> instance with the requested type arguments.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Mapping GetOrAddMapping<TMessage>()
where TMessage : class
{
Type2 key = new(typeof(TMessage), typeof(Unit));
ref IMapping? target = ref this.typesMap.GetOrAddValueRef(key);
target ??= Mapping.Create<TMessage>();
return Unsafe.As<Mapping>(target);
}
/// <summary>
/// Gets the <see cref="Mapping{TToken}"/> instance of currently registered recipients
/// for the combination of types <typeparamref name="TMessage"/> and <typeparamref name="TToken"/>.
/// </summary>
/// <typeparam name="TMessage">The type of message to send.</typeparam>
/// <typeparam name="TToken">The type of token to identify what channel to use to send the message.</typeparam>
/// <returns>A <see cref="Mapping{TToken}"/> instance with the requested type arguments.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Mapping<TToken> GetOrAddMapping<TMessage, TToken>()
where TMessage : class
where TToken : IEquatable<TToken>
{
Type2 key = new(typeof(TMessage), typeof(TToken));
ref IMapping? target = ref this.typesMap.GetOrAddValueRef(key);
target ??= Mapping<TToken>.Create<TMessage>();
return Unsafe.As<Mapping<TToken>>(target);
}
/// <summary>
/// A mapping type representing a link to recipients and their view of handlers per communication channel.
/// </summary>
/// <remarks>
/// This type is a specialization of <see cref="Mapping{TToken}"/> for <see cref="Unit"/> tokens.
/// </remarks>
private sealed class Mapping : Dictionary2<Recipient, object?>, IMapping
{
/// <summary>
/// Initializes a new instance of the <see cref="Mapping"/> class.
/// </summary>
/// <param name="messageType">The message type being used.</param>
private Mapping(Type messageType)
{
TypeArguments = new Type2(messageType, typeof(Unit));
}
/// <summary>
/// Creates a new instance of the <see cref="Mapping"/> class.
/// </summary>
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
/// <returns>A new <see cref="Mapping"/> instance.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Mapping Create<TMessage>()
where TMessage : class
{
return new(typeof(TMessage));
}
/// <inheritdoc/>
public Type2 TypeArguments { get; }
}
/// <summary>
/// A mapping type representing a link to recipients and their view of handlers per communication channel.
/// </summary>
/// <typeparam name="TToken">The type of token to use to pick the messages to receive.</typeparam>
/// <remarks>
/// This type is defined for simplicity and as a workaround for the lack of support for using type aliases
/// over open generic types in C# (using type aliases can only be used for concrete, closed types).
/// </remarks>
private sealed class Mapping<TToken> : Dictionary2<Recipient, Dictionary2<TToken, object?>>, IMapping
where TToken : IEquatable<TToken>
{
/// <summary>
/// Initializes a new instance of the <see cref="Mapping{TToken}"/> class.
/// </summary>
/// <param name="messageType">The message type being used.</param>
private Mapping(Type messageType)
{
TypeArguments = new Type2(messageType, typeof(TToken));
}
/// <summary>
/// Creates a new instance of the <see cref="Mapping{TToken}"/> class.
/// </summary>
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
/// <returns>A new <see cref="Mapping{TToken}"/> instance.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Mapping<TToken> Create<TMessage>()
where TMessage : class
{
return new(typeof(TMessage));
}
/// <inheritdoc/>
public Type2 TypeArguments { get; }
}
/// <summary>
/// An interface for the <see cref="Mapping"/> and <see cref="Mapping{TToken}"/> types which allows to retrieve
/// the type arguments from a given generic instance without having any prior knowledge about those arguments.
/// </summary>
private interface IMapping : IDictionary2<Recipient>
{
/// <summary>
/// Gets the <see cref="Type2"/> instance representing the current type arguments.
/// </summary>
Type2 TypeArguments { get; }
}
/// <summary>
/// A simple type representing a recipient.
/// </summary>
/// <remarks>
/// This type is used to enable fast indexing in each mapping dictionary,
/// since it acts as an external override for the <see cref="GetHashCode"/> and
/// <see cref="Equals(object?)"/> methods for arbitrary objects, removing both
/// the virtual call and preventing instances overriding those methods in this context.
/// Using this type guarantees that all the equality operations are always only done
/// based on reference equality for each registered recipient, regardless of its type.
/// </remarks>
private readonly struct Recipient : IEquatable<Recipient>
{
/// <summary>
/// The registered recipient.
/// </summary>
public readonly object Target;
/// <summary>
/// Initializes a new instance of the <see cref="Recipient"/> struct.
/// </summary>
/// <param name="target">The target recipient instance.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Recipient(object target)
{
this.Target = target;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Recipient other)
{
return ReferenceEquals(this.Target, other.Target);
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is Recipient other && Equals(other);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
{
return RuntimeHelpers.GetHashCode(this.Target);
}
}
/// <summary>
/// Throws an <see cref="InvalidOperationException"/> when trying to add a duplicate handler.
/// </summary>
private static void ThrowInvalidOperationExceptionForDuplicateRegistration()
{
throw new InvalidOperationException("The target recipient has already subscribed to the target message.");
}
}