// =============================================================================== // 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 }