// ===============================================================================
// 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
///
/// Message proxy definition.
///
/// A message proxy can be used to intercept/alter messages and/or
/// marshal delivery actions onto a particular thread.
///
public interface IMessageHubProxy {
///
/// Delivers the specified message.
///
/// The message.
/// The subscription.
void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription);
}
///
/// Default "pass through" proxy.
///
/// Does nothing other than deliver the message.
///
public sealed class MessageHubDefaultProxy : IMessageHubProxy {
private MessageHubDefaultProxy() {
// placeholder
}
///
/// Singleton instance of the proxy.
///
public static MessageHubDefaultProxy Instance { get; } = new MessageHubDefaultProxy();
///
/// Delivers the specified message.
///
/// The message.
/// The subscription.
public void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription) => subscription.Deliver(message);
}
#endregion
#region Hub Interface
///
/// Messenger hub responsible for taking subscriptions/publications and delivering of messages.
///
public interface IMessageHub {
///
/// 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.
///
/// Type of message.
/// Action to invoke when message is delivered.
/// Use strong references to destination and deliveryAction.
/// Proxy to use when delivering the messages.
/// MessageSubscription used to unsubscribing.
MessageHubSubscriptionToken Subscribe(Action deliveryAction, Boolean useStrongReferences, IMessageHubProxy proxy) where TMessage : class, IMessageHubMessage;
///
/// 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.
///
/// Type of message.
/// Action to invoke when message is delivered.
/// The message filter.
/// Use strong references to destination and deliveryAction.
/// Proxy to use when delivering the messages.
///
/// MessageSubscription used to unsubscribing.
///
MessageHubSubscriptionToken Subscribe(Action deliveryAction, Func messageFilter, Boolean useStrongReferences, IMessageHubProxy proxy) where TMessage : class, IMessageHubMessage;
///
/// Unsubscribe from a particular message type.
///
/// Does not throw an exception if the subscription is not found.
///
/// Type of message.
/// Subscription token received from Subscribe.
void Unsubscribe(MessageHubSubscriptionToken subscriptionToken) where TMessage : class, IMessageHubMessage;
///
/// Publish a message to any subscribers.
///
/// Type of message.
/// Message to deliver.
void Publish(TMessage message) where TMessage : class, IMessageHubMessage;
///
/// Publish a message to any subscribers asynchronously.
///
/// Type of message.
/// Message to deliver.
/// A task from Publish action.
Task PublishAsync(TMessage message) where TMessage : class, IMessageHubMessage;
}
#endregion
#region Hub Implementation
///
///
/// 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.
///
/// 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();
/// }
///
/// }
///
///
public sealed class MessageHub : IMessageHub {
#region Private Types and Interfaces
private readonly Object _subscriptionsPadlock = new Object();
private readonly Dictionary> _subscriptions = new Dictionary>();
private class WeakMessageSubscription : IMessageHubSubscription where TMessage : class, IMessageHubMessage {
private readonly WeakReference _deliveryAction;
private readonly WeakReference _messageFilter;
///
/// Initializes a new instance of the class.
///
/// The subscription token.
/// The delivery action.
/// The message filter.
/// subscriptionToken
/// or
/// deliveryAction
/// or
/// messageFilter.
public WeakMessageSubscription(MessageHubSubscriptionToken subscriptionToken, Action deliveryAction, Func 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)this._messageFilter.Target!).Invoke((TMessage)message);
public void Deliver(IMessageHubMessage message) {
if(this._deliveryAction.IsAlive) {
((Action)this._deliveryAction.Target!).Invoke((TMessage)message);
}
}
}
private class StrongMessageSubscription : IMessageHubSubscription where TMessage : class, IMessageHubMessage {
private readonly Action _deliveryAction;
private readonly Func _messageFilter;
///
/// Initializes a new instance of the class.
///
/// The subscription token.
/// The delivery action.
/// The message filter.
/// subscriptionToken
/// or
/// deliveryAction
/// or
/// messageFilter.
public StrongMessageSubscription(MessageHubSubscriptionToken subscriptionToken, Action deliveryAction, Func 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
///
/// 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.
///
/// Type of message.
/// Action to invoke when message is delivered.
/// Use strong references to destination and deliveryAction.
/// Proxy to use when delivering the messages.
/// MessageSubscription used to unsubscribing.
public MessageHubSubscriptionToken Subscribe(Action deliveryAction, Boolean useStrongReferences = true, IMessageHubProxy? proxy = null) where TMessage : class, IMessageHubMessage => this.Subscribe(deliveryAction, m => true, useStrongReferences, proxy);
///
/// 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.
///
/// Type of message.
/// Action to invoke when message is delivered.
/// The message filter.
/// Use strong references to destination and deliveryAction.
/// Proxy to use when delivering the messages.
///
/// MessageSubscription used to unsubscribing.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0068:Empfohlenes Dispose-Muster verwenden", Justification = "")]
public MessageHubSubscriptionToken Subscribe(Action deliveryAction, Func 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? currentSubscriptions)) {
currentSubscriptions = new List();
this._subscriptions[typeof(TMessage)] = currentSubscriptions;
}
MessageHubSubscriptionToken subscriptionToken = new MessageHubSubscriptionToken(this, typeof(TMessage));
IMessageHubSubscription subscription = useStrongReferences ? new StrongMessageSubscription(subscriptionToken, deliveryAction, messageFilter) : (IMessageHubSubscription)new WeakMessageSubscription(subscriptionToken, deliveryAction, messageFilter);
currentSubscriptions.Add(new SubscriptionItem(proxy ?? MessageHubDefaultProxy.Instance, subscription));
return subscriptionToken;
}
}
///
public void Unsubscribe(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? currentSubscriptions)) {
return;
}
List currentlySubscribed = currentSubscriptions.Where(sub => ReferenceEquals(sub.Subscription.SubscriptionToken, subscriptionToken)).ToList();
currentlySubscribed.ForEach(sub => currentSubscriptions.Remove(sub));
}
}
///
/// Publish a message to any subscribers.
///
/// Type of message.
/// Message to deliver.
public void Publish(TMessage message) where TMessage : class, IMessageHubMessage {
if(message == null) {
throw new ArgumentNullException(nameof(message));
}
List currentlySubscribed;
lock(this._subscriptionsPadlock) {
if(!this._subscriptions.TryGetValue(typeof(TMessage), out List? 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
}
});
}
///
/// Publish a message to any subscribers asynchronously.
///
/// Type of message.
/// Message to deliver.
/// A task with the publish.
public Task PublishAsync(TMessage message) where TMessage : class, IMessageHubMessage => Task.Run(() => this.Publish(message));
#endregion
}
#endregion
}