443 lines
18 KiB
C#
443 lines
18 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.
|
|
// ===============================================================================
|
|
|
|
namespace Swan.Messaging
|
|
{
|
|
using System.Threading.Tasks;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
#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,
|
|
bool 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, bool> messageFilter,
|
|
bool 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, bool> messageFilter)
|
|
{
|
|
SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken));
|
|
_deliveryAction = new WeakReference(deliveryAction);
|
|
_messageFilter = new WeakReference(messageFilter);
|
|
}
|
|
|
|
public MessageHubSubscriptionToken SubscriptionToken { get; }
|
|
|
|
public bool ShouldAttemptDelivery(IMessageHubMessage message)
|
|
{
|
|
return _deliveryAction.IsAlive && _messageFilter.IsAlive &&
|
|
((Func<TMessage, bool>) _messageFilter.Target).Invoke((TMessage) message);
|
|
}
|
|
|
|
public void Deliver(IMessageHubMessage message)
|
|
{
|
|
if (_deliveryAction.IsAlive)
|
|
{
|
|
((Action<TMessage>) _deliveryAction.Target).Invoke((TMessage) message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private class StrongMessageSubscription<TMessage> : IMessageHubSubscription
|
|
where TMessage : class, IMessageHubMessage
|
|
{
|
|
private readonly Action<TMessage> _deliveryAction;
|
|
private readonly Func<TMessage, bool> _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, bool> messageFilter)
|
|
{
|
|
SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken));
|
|
_deliveryAction = deliveryAction;
|
|
_messageFilter = messageFilter;
|
|
}
|
|
|
|
public MessageHubSubscriptionToken SubscriptionToken { get; }
|
|
|
|
public bool ShouldAttemptDelivery(IMessageHubMessage message) => _messageFilter.Invoke((TMessage) message);
|
|
|
|
public void Deliver(IMessageHubMessage message) => _deliveryAction.Invoke((TMessage) message);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Subscription dictionary
|
|
|
|
private class SubscriptionItem
|
|
{
|
|
public SubscriptionItem(IMessageHubProxy proxy, IMessageHubSubscription subscription)
|
|
{
|
|
Proxy = proxy;
|
|
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,
|
|
bool useStrongReferences = true,
|
|
IMessageHubProxy? proxy = null)
|
|
where TMessage : class, IMessageHubMessage
|
|
{
|
|
return 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>
|
|
public MessageHubSubscriptionToken Subscribe<TMessage>(
|
|
Action<TMessage> deliveryAction,
|
|
Func<TMessage, bool> messageFilter,
|
|
bool 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 (_subscriptionsPadlock)
|
|
{
|
|
if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions))
|
|
{
|
|
currentSubscriptions = new List<SubscriptionItem>();
|
|
_subscriptions[typeof(TMessage)] = currentSubscriptions;
|
|
}
|
|
|
|
var subscriptionToken = new MessageHubSubscriptionToken(this, typeof(TMessage));
|
|
|
|
IMessageHubSubscription subscription;
|
|
if (useStrongReferences)
|
|
{
|
|
subscription = new StrongMessageSubscription<TMessage>(
|
|
subscriptionToken,
|
|
deliveryAction,
|
|
messageFilter);
|
|
}
|
|
else
|
|
{
|
|
subscription = 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 (_subscriptionsPadlock)
|
|
{
|
|
if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions))
|
|
return;
|
|
|
|
var 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 (_subscriptionsPadlock)
|
|
{
|
|
if (!_subscriptions.TryGetValue(typeof(TMessage), out var 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
|
|
{
|
|
return Task.Run(() => Publish(message));
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
#endregion
|
|
}
|