I wrote a MessageBus class whose only purpose is to deliver messages from object to object without direct link between them.I encountered a problem with duplicate code caused by requirement for same methods with and without generic types, however it does bother me and a better solution would be appreciated.using System;using System.Collections.Generic;using System.Linq;namespace EmeraldTeam.MessageBus{    /// <summary>    /// Class to deliver messages from one object to another without directly linking them.    /// It pushes messages to channels on which objects can subscribe.    /// </summary>    public static class MessageBus    {    /// <summary>    /// Dictionary of message listeners    /// </summary>    private static Dictionary<ChannelKey, object> Listeners { get; } = new Dictionary<ChannelKey, object>();    /// <summary>    /// Send message to the bus    /// </summary>    /// <param name=key>Channel key</param>    /// <param name=arguments>Arguments of message</param>    public static void Send<T>(object key, T arguments)    {        var fullKey = new ChannelKey(key, typeof(T));        if (!Listeners.ContainsKey(fullKey))            return;        var actionList = (List<MessageReaction<T>>)Listeners[fullKey];        foreach (var listener in actionList)        {            listener.Reaction(arguments);        }    }    /// <summary>    /// Send message to the bus    /// </summary>    /// <param name=key>Channel key</param>    public static void Send(object key)    {        var fullKey = new ChannelKey(key);        if (!Listeners.ContainsKey(fullKey))            return;        var actionList = (List<MessageReaction>)Listeners[fullKey];        foreach (var listener in actionList)        {            listener.Reaction();        }    }    /// <summary>    /// Subscribe on messages from channel with given key and given argument type    /// </summary>    /// <param name=subscriber>Subscriber object</param>    /// <param name=key>Channel key</param>    /// <param name=listenerAction>Action to react on message</param>    public static void Subscribe<T>(this object subscriber, object key, Action<T> listenerAction)    {        var fullKey = new ChannelKey(key, typeof(T));        if (!Listeners.ContainsKey(fullKey))            Listeners[fullKey] = new List<MessageReaction<T>>();        var list = (List<MessageReaction<T>>)Listeners[fullKey];        if (list.Any(reaction => reaction.Subscriber.Equals(subscriber)))            return;        list.Add(new MessageReaction<T>(subscriber, listenerAction));    }    /// <summary>    /// Subscribe on messages from channel with given key    /// </summary>    /// <param name=subscriber>Subscriber object</param>    /// <param name=key>Channel key</param>    /// <param name=listenerAction>Action to react on message</param>    public static void Subscribe(this object subscriber, object key, Action listenerAction)    {        var fullKey = new ChannelKey(key);        if (!Listeners.ContainsKey(fullKey))            Listeners[fullKey] = new List<MessageReaction>();        var list = (List<MessageReaction>)Listeners[fullKey];        if (list.Any(reaction => reaction.Subscriber.Equals(subscriber)))            return;        list.Add(new MessageReaction(subscriber, listenerAction));    }    /// <summary>    /// Unsubscribe object from channel by key and argument type    /// </summary>    /// <param name=subscriber>Object to unsubscribe</param>    /// <param name=key>Channel key</param>    public static void UnSubscribe<T>(this object subscriber, object key)    {        var fullKey = new ChannelKey(key, typeof(T));        if (!Listeners.ContainsKey(fullKey))            return;        ((List<MessageReaction<T>>)Listeners[fullKey]).RemoveAll(listener => listener.Subscriber.Equals(subscriber));    }    /// <summary>    /// Unsubscribe object from channel by key    /// </summary>    /// <param name=subscriber>Object to unsubscribe</param>    /// <param name=key>Channel key</param>    public static void UnSubscribe(this object subscriber, object key)    {        var fullKey = new ChannelKey(key);        if (!Listeners.ContainsKey(fullKey))            return;        ((List<MessageReaction>)Listeners[fullKey]).RemoveAll(listener => listener.Subscriber.Equals(subscriber));    }    /// <summary>    /// Class to represent a complex channel key    /// </summary>    private class ChannelKey    {        /// <summary>        /// Channel key        /// </summary>        private object Key { get; }        /// <summary>        /// Channel argument type        /// </summary>        private Type ArgumentType { get; }        public ChannelKey(object key, Type argumentType = null)        {            Key = key;            ArgumentType = argumentType;        }        public override bool Equals(object @object)        {            var anotherKey = @object as ChannelKey;            return anotherKey != null && anotherKey.Key.Equals(Key) && anotherKey.ArgumentType == ArgumentType;        }        public override int GetHashCode()        {            return (Key?.GetHashCode() ?? 0) + (ArgumentType?.GetHashCode() ?? 0);        }    }    /// <summary>    /// Class to represent reaction on message    /// </summary>    /// <typeparam name=T>Type of argument</typeparam>    private class MessageReaction<T>    {        /// <summary>        /// Object which reacts        /// </summary>        public object Subscriber { get; }        /// <summary>        /// Method to react on incoming message        /// </summary>        public Action<T> Reaction { get; }        public MessageReaction(object subscriber, Action<T> reaction)        {            Subscriber = subscriber;            Reaction = reaction;        }    }    /// <summary>    /// Class to represent reaction on message    /// </summary>    private class MessageReaction    {        /// <summary>        /// Object which reacts        /// </summary>        public object Subscriber { get; }        /// <summary>        /// Method to react on incoming message        /// </summary>        public Action Reaction { get; }        public MessageReaction(object subscriber, Action reaction)        {            Subscriber = subscriber;            Reaction = reaction;        }    }}
Message bus in C#
c#;observer pattern
Using ContainsKey() together with the getter of the Item property of a Dictionary<TKey, TValue> should be replaced by a call to TryGetValue() which is faster because the check if the key exists is done by the Item getter too.  Internally these three methods are calling the FindEntry() method to check whether a given key exists. So calling this method only once through the TryGetValue() method should be the way to go for instance like so  public static void Send<T>(object key, T arguments){    var fullKey = new ChannelKey(key, typeof(T));    object value;    if (!Listeners.TryGetValue(fullKey, out value))    {        return;    }    var actionList = (List<MessageReaction<T>>)value;    foreach (var listener in actionList)    {        listener.Reaction(arguments);    }Like you can see I have added braces for the if statement as well. Although they might be optional you should add them always to make your code less error prone.  If you are bothered by duplicated code due to the usage of the generic/non generic version you should maybe think about removing the non generic version. Without seeing the calling code and all the possibilities how the non generic version and the generic version is used its hard to tell if removing the code duplication is possible.In general your code looks good. You are using well named methods (except for Reaction IMO) and the documentation makes it easy to understand what the code is doing.  I wouldn't have the Listeners as a private property but that's a matter of taste. I personally would prefer just a private static readonly field.  I don't quite see why the MessageReaction should know to which Subscriber it belongs. If the MessageReaction would want to talk to its parent  it should use events so there is no need to couple this objects that much.  