372 lines
16 KiB
C#
372 lines
16 KiB
C#
// ===============================================================================
|
|
// TinyIoC - TinyMessenger
|
|
//
|
|
// A simple messenger/event aggregator.
|
|
//
|
|
// https://github.com/grumpydev/TinyIoC/blob/master/src/TinyIoC/TinyMessenger.cs
|
|
// ===============================================================================
|
|
// Copyright © Steven Robbins. All rights reserved.
|
|
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
|
|
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
|
|
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
// FITNESS FOR A PARTICULAR PURPOSE.
|
|
// ===============================================================================
|
|
#nullable enable
|
|
using System.Threading.Tasks;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace Swan.Messaging {
|
|
#region Message Types / Interfaces
|
|
|
|
/// <summary>
|
|
/// Message proxy definition.
|
|
///
|
|
/// A message proxy can be used to intercept/alter messages and/or
|
|
/// marshal delivery actions onto a particular thread.
|
|
/// </summary>
|
|
public interface IMessageHubProxy {
|
|
/// <summary>
|
|
/// Delivers the specified message.
|
|
/// </summary>
|
|
/// <param name="message">The message.</param>
|
|
/// <param name="subscription">The subscription.</param>
|
|
void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Default "pass through" proxy.
|
|
///
|
|
/// Does nothing other than deliver the message.
|
|
/// </summary>
|
|
public sealed class MessageHubDefaultProxy : IMessageHubProxy {
|
|
private MessageHubDefaultProxy() {
|
|
// placeholder
|
|
}
|
|
|
|
/// <summary>
|
|
/// Singleton instance of the proxy.
|
|
/// </summary>
|
|
public static MessageHubDefaultProxy Instance { get; } = new MessageHubDefaultProxy();
|
|
|
|
/// <summary>
|
|
/// Delivers the specified message.
|
|
/// </summary>
|
|
/// <param name="message">The message.</param>
|
|
/// <param name="subscription">The subscription.</param>
|
|
public void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription) => subscription.Deliver(message);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Hub Interface
|
|
|
|
/// <summary>
|
|
/// Messenger hub responsible for taking subscriptions/publications and delivering of messages.
|
|
/// </summary>
|
|
public interface IMessageHub {
|
|
/// <summary>
|
|
/// Subscribe to a message type with the given destination and delivery action.
|
|
/// Messages will be delivered via the specified proxy.
|
|
///
|
|
/// All messages of this type will be delivered.
|
|
/// </summary>
|
|
/// <typeparam name="TMessage">Type of message.</typeparam>
|
|
/// <param name="deliveryAction">Action to invoke when message is delivered.</param>
|
|
/// <param name="useStrongReferences">Use strong references to destination and deliveryAction.</param>
|
|
/// <param name="proxy">Proxy to use when delivering the messages.</param>
|
|
/// <returns>MessageSubscription used to unsubscribing.</returns>
|
|
MessageHubSubscriptionToken Subscribe<TMessage>(Action<TMessage> deliveryAction, Boolean useStrongReferences, IMessageHubProxy proxy) where TMessage : class, IMessageHubMessage;
|
|
|
|
/// <summary>
|
|
/// Subscribe to a message type with the given destination and delivery action with the given filter.
|
|
/// Messages will be delivered via the specified proxy.
|
|
/// All references are held with WeakReferences
|
|
/// Only messages that "pass" the filter will be delivered.
|
|
/// </summary>
|
|
/// <typeparam name="TMessage">Type of message.</typeparam>
|
|
/// <param name="deliveryAction">Action to invoke when message is delivered.</param>
|
|
/// <param name="messageFilter">The message filter.</param>
|
|
/// <param name="useStrongReferences">Use strong references to destination and deliveryAction.</param>
|
|
/// <param name="proxy">Proxy to use when delivering the messages.</param>
|
|
/// <returns>
|
|
/// MessageSubscription used to unsubscribing.
|
|
/// </returns>
|
|
MessageHubSubscriptionToken Subscribe<TMessage>(Action<TMessage> deliveryAction, Func<TMessage, Boolean> messageFilter, Boolean useStrongReferences, IMessageHubProxy proxy) where TMessage : class, IMessageHubMessage;
|
|
|
|
/// <summary>
|
|
/// Unsubscribe from a particular message type.
|
|
///
|
|
/// Does not throw an exception if the subscription is not found.
|
|
/// </summary>
|
|
/// <typeparam name="TMessage">Type of message.</typeparam>
|
|
/// <param name="subscriptionToken">Subscription token received from Subscribe.</param>
|
|
void Unsubscribe<TMessage>(MessageHubSubscriptionToken subscriptionToken) where TMessage : class, IMessageHubMessage;
|
|
|
|
/// <summary>
|
|
/// Publish a message to any subscribers.
|
|
/// </summary>
|
|
/// <typeparam name="TMessage">Type of message.</typeparam>
|
|
/// <param name="message">Message to deliver.</param>
|
|
void Publish<TMessage>(TMessage message) where TMessage : class, IMessageHubMessage;
|
|
|
|
/// <summary>
|
|
/// Publish a message to any subscribers asynchronously.
|
|
/// </summary>
|
|
/// <typeparam name="TMessage">Type of message.</typeparam>
|
|
/// <param name="message">Message to deliver.</param>
|
|
/// <returns>A task from Publish action.</returns>
|
|
Task PublishAsync<TMessage>(TMessage message) where TMessage : class, IMessageHubMessage;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Hub Implementation
|
|
|
|
/// <inheritdoc />
|
|
/// <example>
|
|
/// The following code describes how to use a MessageHub. Both the
|
|
/// subscription and the message sending are done in the same place but this is only for explanatory purposes.
|
|
/// <code>
|
|
/// class Example
|
|
/// {
|
|
/// using Swan;
|
|
/// using Swan.Components;
|
|
///
|
|
/// static void Main()
|
|
/// {
|
|
/// // using DependencyContainer to create an instance of MessageHub
|
|
/// var messageHub = DependencyContainer
|
|
/// .Current
|
|
/// .Resolve<IMessageHub>() as MessageHub;
|
|
///
|
|
/// // create an instance of the publisher class
|
|
/// // which has a string as its content
|
|
/// var message = new MessageHubGenericMessage<string>(new object(), "SWAN");
|
|
///
|
|
/// // subscribe to the publisher's event
|
|
/// // and just print out the content which is a string
|
|
/// // a token is returned which can be used to unsubscribe later on
|
|
/// var token = messageHub
|
|
/// .Subscribe<MessageHubGenericMessage<string>>(m => m.Content.Info());
|
|
///
|
|
/// // publish the message described above which is
|
|
/// // the string 'SWAN'
|
|
/// messageHub.Publish(message);
|
|
///
|
|
/// // unsuscribe, we will no longer receive any messages
|
|
/// messageHub.Unsubscribe<MessageHubGenericMessage<string>>(token);
|
|
///
|
|
/// Terminal.Flush();
|
|
/// }
|
|
///
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public sealed class MessageHub : IMessageHub {
|
|
#region Private Types and Interfaces
|
|
|
|
private readonly Object _subscriptionsPadlock = new Object();
|
|
|
|
private readonly Dictionary<Type, List<SubscriptionItem>> _subscriptions = new Dictionary<Type, List<SubscriptionItem>>();
|
|
|
|
private class WeakMessageSubscription<TMessage> : IMessageHubSubscription where TMessage : class, IMessageHubMessage {
|
|
private readonly WeakReference _deliveryAction;
|
|
private readonly WeakReference _messageFilter;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="WeakMessageSubscription{TMessage}" /> class.
|
|
/// </summary>
|
|
/// <param name="subscriptionToken">The subscription token.</param>
|
|
/// <param name="deliveryAction">The delivery action.</param>
|
|
/// <param name="messageFilter">The message filter.</param>
|
|
/// <exception cref="ArgumentNullException">subscriptionToken
|
|
/// or
|
|
/// deliveryAction
|
|
/// or
|
|
/// messageFilter.</exception>
|
|
public WeakMessageSubscription(MessageHubSubscriptionToken subscriptionToken, Action<TMessage> deliveryAction, Func<TMessage, Boolean> messageFilter) {
|
|
this.SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken));
|
|
this._deliveryAction = new WeakReference(deliveryAction);
|
|
this._messageFilter = new WeakReference(messageFilter);
|
|
}
|
|
|
|
public MessageHubSubscriptionToken SubscriptionToken {
|
|
get;
|
|
}
|
|
|
|
public Boolean ShouldAttemptDelivery(IMessageHubMessage message) => this._deliveryAction.IsAlive && this._messageFilter.IsAlive && ((Func<TMessage, Boolean>)this._messageFilter.Target!).Invoke((TMessage)message);
|
|
|
|
public void Deliver(IMessageHubMessage message) {
|
|
if(this._deliveryAction.IsAlive) {
|
|
((Action<TMessage>)this._deliveryAction.Target!).Invoke((TMessage)message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private class StrongMessageSubscription<TMessage> : IMessageHubSubscription where TMessage : class, IMessageHubMessage {
|
|
private readonly Action<TMessage> _deliveryAction;
|
|
private readonly Func<TMessage, Boolean> _messageFilter;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="StrongMessageSubscription{TMessage}" /> class.
|
|
/// </summary>
|
|
/// <param name="subscriptionToken">The subscription token.</param>
|
|
/// <param name="deliveryAction">The delivery action.</param>
|
|
/// <param name="messageFilter">The message filter.</param>
|
|
/// <exception cref="ArgumentNullException">subscriptionToken
|
|
/// or
|
|
/// deliveryAction
|
|
/// or
|
|
/// messageFilter.</exception>
|
|
public StrongMessageSubscription(MessageHubSubscriptionToken subscriptionToken, Action<TMessage> deliveryAction, Func<TMessage, Boolean> messageFilter) {
|
|
this.SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken));
|
|
this._deliveryAction = deliveryAction;
|
|
this._messageFilter = messageFilter;
|
|
}
|
|
|
|
public MessageHubSubscriptionToken SubscriptionToken {
|
|
get;
|
|
}
|
|
|
|
public Boolean ShouldAttemptDelivery(IMessageHubMessage message) => this._messageFilter.Invoke((TMessage)message);
|
|
|
|
public void Deliver(IMessageHubMessage message) => this._deliveryAction.Invoke((TMessage)message);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Subscription dictionary
|
|
|
|
private class SubscriptionItem {
|
|
public SubscriptionItem(IMessageHubProxy proxy, IMessageHubSubscription subscription) {
|
|
this.Proxy = proxy;
|
|
this.Subscription = subscription;
|
|
}
|
|
|
|
public IMessageHubProxy Proxy {
|
|
get;
|
|
}
|
|
public IMessageHubSubscription Subscription {
|
|
get;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public API
|
|
|
|
/// <summary>
|
|
/// Subscribe to a message type with the given destination and delivery action.
|
|
/// Messages will be delivered via the specified proxy.
|
|
///
|
|
/// All messages of this type will be delivered.
|
|
/// </summary>
|
|
/// <typeparam name="TMessage">Type of message.</typeparam>
|
|
/// <param name="deliveryAction">Action to invoke when message is delivered.</param>
|
|
/// <param name="useStrongReferences">Use strong references to destination and deliveryAction. </param>
|
|
/// <param name="proxy">Proxy to use when delivering the messages.</param>
|
|
/// <returns>MessageSubscription used to unsubscribing.</returns>
|
|
public MessageHubSubscriptionToken Subscribe<TMessage>(Action<TMessage> deliveryAction, Boolean useStrongReferences = true, IMessageHubProxy? proxy = null) where TMessage : class, IMessageHubMessage => this.Subscribe(deliveryAction, m => true, useStrongReferences, proxy);
|
|
|
|
|
|
/// <summary>
|
|
/// Subscribe to a message type with the given destination and delivery action with the given filter.
|
|
/// Messages will be delivered via the specified proxy.
|
|
/// All references are held with WeakReferences
|
|
/// Only messages that "pass" the filter will be delivered.
|
|
/// </summary>
|
|
/// <typeparam name="TMessage">Type of message.</typeparam>
|
|
/// <param name="deliveryAction">Action to invoke when message is delivered.</param>
|
|
/// <param name="messageFilter">The message filter.</param>
|
|
/// <param name="useStrongReferences">Use strong references to destination and deliveryAction.</param>
|
|
/// <param name="proxy">Proxy to use when delivering the messages.</param>
|
|
/// <returns>
|
|
/// MessageSubscription used to unsubscribing.
|
|
/// </returns>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0068:Empfohlenes Dispose-Muster verwenden", Justification = "<Ausstehend>")]
|
|
public MessageHubSubscriptionToken Subscribe<TMessage>(Action<TMessage> deliveryAction, Func<TMessage, Boolean> messageFilter, Boolean useStrongReferences = true, IMessageHubProxy? proxy = null) where TMessage : class, IMessageHubMessage {
|
|
if(deliveryAction == null) {
|
|
throw new ArgumentNullException(nameof(deliveryAction));
|
|
}
|
|
|
|
if(messageFilter == null) {
|
|
throw new ArgumentNullException(nameof(messageFilter));
|
|
}
|
|
|
|
lock(this._subscriptionsPadlock) {
|
|
if(!this._subscriptions.TryGetValue(typeof(TMessage), out List<SubscriptionItem>? currentSubscriptions)) {
|
|
currentSubscriptions = new List<SubscriptionItem>();
|
|
this._subscriptions[typeof(TMessage)] = currentSubscriptions;
|
|
}
|
|
|
|
MessageHubSubscriptionToken subscriptionToken = new MessageHubSubscriptionToken(this, typeof(TMessage));
|
|
|
|
IMessageHubSubscription subscription = useStrongReferences ? new StrongMessageSubscription<TMessage>(subscriptionToken, deliveryAction, messageFilter) : (IMessageHubSubscription)new WeakMessageSubscription<TMessage>(subscriptionToken, deliveryAction, messageFilter);
|
|
|
|
currentSubscriptions.Add(new SubscriptionItem(proxy ?? MessageHubDefaultProxy.Instance, subscription));
|
|
|
|
return subscriptionToken;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Unsubscribe<TMessage>(MessageHubSubscriptionToken subscriptionToken) where TMessage : class, IMessageHubMessage {
|
|
if(subscriptionToken == null) {
|
|
throw new ArgumentNullException(nameof(subscriptionToken));
|
|
}
|
|
|
|
lock(this._subscriptionsPadlock) {
|
|
if(!this._subscriptions.TryGetValue(typeof(TMessage), out List<SubscriptionItem>? currentSubscriptions)) {
|
|
return;
|
|
}
|
|
|
|
List<SubscriptionItem> currentlySubscribed = currentSubscriptions.Where(sub => ReferenceEquals(sub.Subscription.SubscriptionToken, subscriptionToken)).ToList();
|
|
|
|
currentlySubscribed.ForEach(sub => currentSubscriptions.Remove(sub));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Publish a message to any subscribers.
|
|
/// </summary>
|
|
/// <typeparam name="TMessage">Type of message.</typeparam>
|
|
/// <param name="message">Message to deliver.</param>
|
|
public void Publish<TMessage>(TMessage message) where TMessage : class, IMessageHubMessage {
|
|
if(message == null) {
|
|
throw new ArgumentNullException(nameof(message));
|
|
}
|
|
|
|
List<SubscriptionItem> currentlySubscribed;
|
|
lock(this._subscriptionsPadlock) {
|
|
if(!this._subscriptions.TryGetValue(typeof(TMessage), out List<SubscriptionItem>? currentSubscriptions)) {
|
|
return;
|
|
}
|
|
|
|
currentlySubscribed = currentSubscriptions.Where(sub => sub.Subscription.ShouldAttemptDelivery(message)).ToList();
|
|
}
|
|
|
|
currentlySubscribed.ForEach(sub => {
|
|
try {
|
|
sub.Proxy.Deliver(message, sub.Subscription);
|
|
} catch {
|
|
// Ignore any errors and carry on
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Publish a message to any subscribers asynchronously.
|
|
/// </summary>
|
|
/// <typeparam name="TMessage">Type of message.</typeparam>
|
|
/// <param name="message">Message to deliver.</param>
|
|
/// <returns>A task with the publish.</returns>
|
|
public Task PublishAsync<TMessage>(TMessage message) where TMessage : class, IMessageHubMessage => Task.Run(() => this.Publish(message));
|
|
|
|
#endregion
|
|
}
|
|
|
|
#endregion
|
|
}
|