-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Messenger.cs
685 lines (603 loc) · 33.3 KB
/
Messenger.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
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
// 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.Runtime.CompilerServices;
using System.Threading;
using Microsoft.Collections.Extensions;
namespace Microsoft.Toolkit.Mvvm.Messaging
{
/// <summary>
/// A type that can be used to exchange messages between different objects.
/// This can be useful to decouple different modules of an application without having to keep strong
/// references to types being referenced. It is also possible to send messages to specific channels, uniquely
/// identified by a token, and to have different messengers in different sections of an applications.
/// In order to use the <see cref="IMessenger"/> functionalities, first define a message type, like so:
/// <code>
/// public sealed class LoginCompletedMessage { }
/// </code>
/// Then, register your a recipient for this message:
/// <code>
/// Messenger.Default.Register<LoginCompletedMessage>(this, m =>
/// {
/// // Handle the message here...
/// });
/// </code>
/// Finally, send a message when needed, like so:
/// <code>
/// Messenger.Default.Send<LoginCompletedMessage>();
/// </code>
/// Additionally, the method group syntax can also be used to specify the action
/// to invoke when receiving a message, if a method with the right signature is available
/// in the current scope. This is helpful to keep the registration and handling logic separate.
/// Following up from the previous example, consider a class having this method:
/// <code>
/// private void Receive(LoginCompletedMessage message)
/// {
/// // Handle the message there
/// }
/// </code>
/// The registration can then be performed in a single line like so:
/// <code>
/// Messenger.Default.Register<LoginCompletedMessage>(this, Receive);
/// </code>
/// The C# compiler will automatically convert that expression to an <see cref="Action{T}"/> instance
/// compatible with the <see cref="MessengerExtensions.Register{T}(IMessenger,object,Action{T})"/> method.
/// This will also work if multiple overloads of that method are available, each handling a different
/// message type: the C# compiler will automatically pick the right one for the current message type.
/// For info on the other available features, check the <see cref="IMessenger"/> interface.
/// </summary>
public sealed class Messenger : IMessenger
{
// The Messenger class uses the following logic to link stored instances together:
// --------------------------------------------------------------------------------------------------------
// DictionarySlim<Recipient, HashSet<IMapping>> recipientsMap;
// | \________________[*]IDictionarySlim<Recipient, IDictionarySlim<TToken>>
// | \___ / / /
// | ________(recipients registrations)___________\________/ / __/
// | / _______(channel registrations)_____\___________________/ /
// | / / \ /
// DictionarySlim<Recipient, DictionarySlim<TToken, Action<TMessage>>> mapping = Mapping<TMessage, TToken>
// / / \ / /
// ___(Type2.tToken)____/ / \______/___________________/
// /________________(Type2.tMessage)____/ /
// / ________________________________________/
// / /
// DictionarySlim<Type2, IMapping> typesMap;
// --------------------------------------------------------------------------------------------------------
// Each combination of <TMessage, TToken> results in a concrete Mapping<TMessage, TToken> type, which holds
// the references from registered recipients to handlers. The handlers are stored in a <TToken, Action<TMessage>>
// dictionary, so that each recipient can have up to one registered handler for a given token, for each
// message type. 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. 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). 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
// IDictionarySlim<Recipient, Token>, with any token type currently in use by that recipient. 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 DictionarySlim<Recipient, HashSet<IMapping>> recipientsMap = new DictionarySlim<Recipient, HashSet<IMapping>>();
/// <summary>
/// The <see cref="Mapping{TMessage,TToken}"/> instance for types combination.
/// </summary>
/// <remarks>
/// The values are just of type <see cref="IDictionarySlim{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{TMessage,TToken}"/> class for each pair of generic arguments in use.
/// </remarks>
private readonly DictionarySlim<Type2, IMapping> typesMap = new DictionarySlim<Type2, IMapping>();
/// <summary>
/// Gets the default <see cref="Messenger"/> instance.
/// </summary>
public static Messenger Default { get; } = new Messenger();
/// <inheritdoc/>
public bool IsRegistered<TMessage, TToken>(object recipient, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
lock (this.recipientsMap)
{
if (!TryGetMapping(out Mapping<TMessage, TToken>? mapping))
{
return false;
}
var key = new Recipient(recipient);
return mapping!.ContainsKey(key);
}
}
/// <inheritdoc/>
public void Register<TMessage, TToken>(object recipient, TToken token, Action<TMessage> action)
where TMessage : class
where TToken : IEquatable<TToken>
{
lock (this.recipientsMap)
{
// Get the <TMessage, TToken> registration list for this recipient
Mapping<TMessage, TToken> mapping = GetOrAddMapping<TMessage, TToken>();
var key = new Recipient(recipient);
ref DictionarySlim<TToken, Action<TMessage>>? map = ref mapping.GetOrAddValueRef(key);
map ??= new DictionarySlim<TToken, Action<TMessage>>();
// Add the new registration entry
ref Action<TMessage>? handler = ref map.GetOrAddValueRef(token);
if (!(handler is null))
{
ThrowInvalidOperationExceptionForDuplicateRegistration();
}
handler = action;
// Update the total counter for handlers for the current type parameters
mapping.TotalHandlersCount++;
// 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)
{
lock (this.recipientsMap)
{
// If the recipient has no registered messages at all, ignore
var key = new Recipient(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, out object? handlersMap))
{
// If this branch is taken, it means the target recipient to unregister
// had at least one registered handler for the current <TToken, TMessage>
// pair of type parameters, which here is masked out by the IMapping interface.
// Before removing the handlers, we need to retrieve the count of how many handlers
// are being removed, in order to update the total counter for the mapping.
// Just casting the dictionary to the base interface and accessing the Count
// property directly gives us O(1) access time to retrieve this count.
// The handlers map is the IDictionary<TToken, TMessage> instance for the mapping.
int handlersCount = Unsafe.As<IDictionarySlim>(handlersMap).Count;
mapping.TotalHandlersCount -= handlersCount;
if (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.Remove(mapping.TypeArguments);
}
}
}
// Remove the associated set in the recipients map
this.recipientsMap.Remove(key);
}
}
/// <inheritdoc/>
public void UnregisterAll<TToken>(object recipient, TToken token)
where TToken : IEquatable<TToken>
{
bool lockTaken = false;
IDictionarySlim<Recipient, IDictionarySlim<TToken>>[]? 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
var key = new Recipient(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.
maps = ArrayPool<IDictionarySlim<Recipient, IDictionarySlim<TToken>>>.Shared.Rent(set!.Count);
foreach (IMapping item in set)
{
// Select all mappings using the same token type
if (item is IDictionarySlim<Recipient, IDictionarySlim<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 (IDictionarySlim<Recipient, IDictionarySlim<TToken>> map in maps.AsSpan(0, i))
{
// 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.
IDictionarySlim<TToken> holder = map[key];
// Try to remove the registered handler for the input token,
// for the current message type (unknown from here).
if (holder.Remove(token))
{
// As above, we need to update the total number of registered handlers for the map.
// In this case we also know that the current TToken type parameter is of interest
// for the current method, as we're only unsubscribing handlers using that token.
// This is because we're already working on the final <TToken, TMessage> mapping,
// which associates a single handler with a given token, for a given recipient.
// This means that we don't have to retrieve the count to subtract in this case,
// we're just removing a single handler at a time. So, we just decrement the total.
Unsafe.As<IMapping>(map).TotalHandlersCount--;
if (holder.Count == 0)
{
// If the map is empty, remove the recipient entirely from its container
map.Remove(key);
if (map.Count == 0)
{
// If no handlers are left at all for the recipient, across all
// message types and token types, remove the set of mappings
// entirely for the current recipient, and lost the strong
// reference to it as well. This is the same situation that
// would've been achieved by just calling UnregisterAll(recipient).
set.Remove(Unsafe.As<IMapping>(map));
if (set.Count == 0)
{
this.recipientsMap.Remove(key);
}
}
}
}
}
}
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 null))
{
maps.AsSpan(0, i).Clear();
ArrayPool<IDictionarySlim<Recipient, IDictionarySlim<TToken>>>.Shared.Return(maps);
}
}
}
/// <inheritdoc/>
public void Unregister<TMessage, TToken>(object recipient, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
lock (this.recipientsMap)
{
// Get the registration list, if available
if (!TryGetMapping(out Mapping<TMessage, TToken>? mapping))
{
return;
}
var key = new Recipient(recipient);
if (!mapping!.TryGetValue(key, out DictionarySlim<TToken, Action<TMessage>>? dictionary))
{
return;
}
// Remove the target handler
if (dictionary!.Remove(token))
{
// Decrement the total count, as above
mapping.TotalHandlersCount--;
// 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).
if (dictionary.Count == 0)
{
mapping.Remove(key);
HashSet<IMapping> set = this.recipientsMap[key];
set.Remove(mapping);
if (set.Count == 0)
{
this.recipientsMap.Remove(key);
}
}
}
}
}
/// <inheritdoc/>
public TMessage Send<TMessage, TToken>(TMessage message, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
Action<TMessage>[] entries;
int i = 0;
lock (this.recipientsMap)
{
// Check whether there are any registered recipients
if (!TryGetMapping(out Mapping<TMessage, TToken>? mapping))
{
return message;
}
// 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.
entries = ArrayPool<Action<TMessage>>.Shared.Rent(mapping!.TotalHandlersCount);
// Copy the handlers to the local collection.
// Both types being enumerate expose a struct enumerator,
// so we're not actually allocating the enumerator here.
// 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 actions in the rented buffer.
var 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())
{
var pairsEnumerator = mappingEnumerator.Value.GetEnumerator();
while (pairsEnumerator.MoveNext())
{
// Only select the ones with a matching token
if (pairsEnumerator.Key.Equals(token))
{
entries[i++] = pairsEnumerator.Value;
}
}
}
}
try
{
// Invoke all the necessary handlers on the local copy of entries
foreach (var entry in entries.AsSpan(0, i))
{
entry(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.
entries.AsSpan(0, i).Clear();
ArrayPool<Action<TMessage>>.Shared.Return(entries);
}
return message;
}
/// <inheritdoc/>
public void Reset()
{
lock (this.recipientsMap)
{
this.recipientsMap.Clear();
this.typesMap.Clear();
}
}
/// <summary>
/// Tries to get the <see cref="Mapping{TMessage,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{TMessage,TToken}"/> instance, if found.</param>
/// <returns>Whether or not the required <see cref="Mapping{TMessage,TToken}"/> instance was found.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryGetMapping<TMessage, TToken>(out Mapping<TMessage, TToken>? mapping)
where TMessage : class
where TToken : IEquatable<TToken>
{
var key = new Type2(typeof(TMessage), typeof(TToken));
if (this.typesMap.TryGetValue(key, out IMapping? target))
{
// This method and the ones above 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<TMessage, TToken>>(target);
return true;
}
mapping = null;
return false;
}
/// <summary>
/// Gets the <see cref="Mapping{TMessage,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{TMessage,TToken}"/> instance with the requested type arguments.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Mapping<TMessage, TToken> GetOrAddMapping<TMessage, TToken>()
where TMessage : class
where TToken : IEquatable<TToken>
{
var key = new Type2(typeof(TMessage), typeof(TToken));
ref IMapping? target = ref this.typesMap.GetOrAddValueRef(key);
target ??= new Mapping<TMessage, TToken>();
return Unsafe.As<Mapping<TMessage, TToken>>(target);
}
/// <summary>
/// A mapping type representing a link to recipients and their view of handlers per communication channel.
/// </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>
/// <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<TMessage, TToken> : DictionarySlim<Recipient, DictionarySlim<TToken, Action<TMessage>>>, IMapping
where TMessage : class
where TToken : IEquatable<TToken>
{
/// <summary>
/// Initializes a new instance of the <see cref="Mapping{TMessage, TToken}"/> class.
/// </summary>
public Mapping()
{
TypeArguments = new Type2(typeof(TMessage), typeof(TToken));
}
/// <inheritdoc/>
public Type2 TypeArguments { get; }
/// <inheritdoc/>
public int TotalHandlersCount { get; set; }
}
/// <summary>
/// An interface for the <see cref="Mapping{TMessage,TToken}"/> type which allows to retrieve the type
/// arguments from a given generic instance without having any prior knowledge about those arguments.
/// </summary>
private interface IMapping : IDictionarySlim<Recipient>
{
/// <summary>
/// Gets the <see cref="Type2"/> instance representing the current type arguments.
/// </summary>
Type2 TypeArguments { get; }
/// <summary>
/// Gets or sets the total number of handlers in the current instance.
/// </summary>
int TotalHandlersCount { get; set; }
}
/// <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>
private 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>
/// A simple type representing an immutable pair of types.
/// </summary>
/// <remarks>
/// This type replaces a simple <see cref="ValueTuple{T1,T2}"/> as it's faster in its
/// <see cref="GetHashCode"/> and <see cref="IEquatable{T}.Equals(T)"/> methods, and because
/// unlike a value tuple it exposes its fields as immutable. Additionally, the
/// <see cref="tMessage"/> and <see cref="tToken"/> fields provide additional clarity reading
/// the code compared to <see cref="ValueTuple{T1,T2}.Item1"/> and <see cref="ValueTuple{T1,T2}.Item2"/>.
/// </remarks>
private readonly struct Type2 : IEquatable<Type2>
{
/// <summary>
/// The type of registered message.
/// </summary>
private readonly Type tMessage;
/// <summary>
/// The type of registration token.
/// </summary>
private readonly Type tToken;
/// <summary>
/// Initializes a new instance of the <see cref="Type2"/> struct.
/// </summary>
/// <param name="tMessage">The type of registered message.</param>
/// <param name="tToken">The type of registration token.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Type2(Type tMessage, Type tToken)
{
this.tMessage = tMessage;
this.tToken = tToken;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Type2 other)
{
// We can't just use reference equality, as that's technically not guaranteed
// to work and might fail in very rare cases (eg. with type forwarding between
// different assemblies). Instead, we can use the == operator to compare for
// equality, which still avoids the callvirt overhead of calling Type.Equals,
// and is also implemented as a JIT intrinsic on runtimes such as .NET Core.
return
this.tMessage == other.tMessage &&
this.tToken == other.tToken;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is Type2 other && Equals(other);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
{
unchecked
{
// To combine the two hashes, we can simply use the fast djb2 hash algorithm.
// This is not a problem in this case since we already know that the base
// RuntimeHelpers.GetHashCode method is providing hashes with a good enough distribution.
int hash = RuntimeHelpers.GetHashCode(this.tMessage);
hash = (hash << 5) + hash;
hash += RuntimeHelpers.GetHashCode(this.tToken);
return hash;
}
}
}
/// <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");
}
}
}