diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c9e599 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/[Bb]in/ +/[Oo]bj/ +/[Bb]uild/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..753f43d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/net6.0/MessageBus.exe", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "internalConsole" + }, + ], +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fc66b74 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "MessageBus.sln" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..e1d7a99 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,14 @@ +{ + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/MessageBus.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Example/Example.cs b/Example/Example.cs new file mode 100644 index 0000000..861652d --- /dev/null +++ b/Example/Example.cs @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------ +// MIT License +// +// Copyright (c) 2024 Tobias Barendt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//------------------------------------------------------------------------------ + +public class Example +{ + //-------------------------------------------------------------------------- + // Messages + public delegate void ExampleMessage(); + public delegate void ExampleArguments(int x, int y); + + //-------------------------------------------------------------------------- + // Constructor + //-------------------------------------------------------------------------- + public Example() + { + // Example of how to use it unscoped + UnscopedExample(); + + // Example on how to use it with a scope + ScopedExample(); + + + } + + //-------------------------------------------------------------------------- + // UnscopedExample + //-------------------------------------------------------------------------- + private void UnscopedExample() + { + //---------------------------------------------------------------------- + // Subscribe to a few messages + MessageBus.Subscribe(OnExampleMessage); + MessageBus.Subscribe(OnExampleArguments); + + + //---------------------------------------------------------------------- + // Dispatch messages + MessageBus.Dispatch(); + MessageBus.Dispatch(100, 200); + + //---------------------------------------------------------------------- + // Remove subscriptions + MessageBus.Unsubscribe(OnExampleMessage); + MessageBus.Unsubscribe(OnExampleArguments); + } + + + //-------------------------------------------------------------------------- + // ScopedExample + //-------------------------------------------------------------------------- + private void ScopedExample() + { + //---------------------------------------------------------------------- + // Subscribe to a few messages + MessageBus.Subscribe("ExampleScope", OnExampleMessage); + MessageBus.Subscribe("ExampleScope", OnExampleArguments); + + + //---------------------------------------------------------------------- + // Dispatch messages + var scopedDispatcher = MessageBus.GetDispatcher("ExampleScope"); + + scopedDispatcher.Dispatch(); + scopedDispatcher.Dispatch(64, 128); + + //---------------------------------------------------------------------- + // Remove subscriptions + MessageBus.Unsubscribe("ExampleScope", OnExampleMessage); + MessageBus.Unsubscribe("ExampleScope", OnExampleArguments); + } + + //-------------------------------------------------------------------------- + // OnExampleMessage + //-------------------------------------------------------------------------- + private void OnExampleMessage() + { + Console.WriteLine("OnExampleMessage"); + } + + //-------------------------------------------------------------------------- + // OnExampleArguments + //-------------------------------------------------------------------------- + private void OnExampleArguments(int x, int y) + { + Console.WriteLine("ExampleArguments with " + x + " and " + y); + } +} \ No newline at end of file diff --git a/MessageBus.csproj b/MessageBus.csproj new file mode 100644 index 0000000..74abf5c --- /dev/null +++ b/MessageBus.csproj @@ -0,0 +1,10 @@ + + + + Exe + net6.0 + enable + enable + + + diff --git a/MessageBus.sln b/MessageBus.sln new file mode 100644 index 0000000..c911c04 --- /dev/null +++ b/MessageBus.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessageBus", "MessageBus.csproj", "{AC802790-5432-4DFD-B9E0-FB66DA790C92}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AC802790-5432-4DFD-B9E0-FB66DA790C92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC802790-5432-4DFD-B9E0-FB66DA790C92}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC802790-5432-4DFD-B9E0-FB66DA790C92}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC802790-5432-4DFD-B9E0-FB66DA790C92}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/MessageBus/MessageBus.cs b/MessageBus/MessageBus.cs new file mode 100644 index 0000000..8e0f66e --- /dev/null +++ b/MessageBus/MessageBus.cs @@ -0,0 +1,204 @@ +//------------------------------------------------------------------------------ +// MIT License +// +// Copyright (c) 2024 Tobias Barendt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//------------------------------------------------------------------------------ +using System.Collections.Generic; +using System.Diagnostics; + +//------------------------------------------------------------------------------ +// MessageBus +// +// The message bus is a central point for communication between different parts +// of the application. It is used to decouple different parts of the application +// and to allow for a more modular design. +// +public class MessageBus +{ + //-------------------------------------------------------------------------- + // Singleton instance + private static MessageBus instance = new MessageBus(); + + //-------------------------------------------------------------------------- + // Message Scopes + private Dictionary m_scopes = new(); + + //-------------------------------------------------------------------------- + // GetDispatcher + // + // Returns the message dispatcher for the given scope. IF scoped does not + // already exist it will be created. + // + //-------------------------------------------------------------------------- + public static MessageDispatcher GetDispatcher() => instance.Internal_GetDispatcher(""); + public static MessageDispatcher GetDispatcher(string scope) => instance.Internal_GetDispatcher(scope); + private MessageDispatcher Internal_GetDispatcher(string scope) + { + if(m_scopes.TryGetValue(scope, out var dispatcher)) + return dispatcher; + + m_scopes[scope] = new MessageDispatcher(scope); + return m_scopes[scope]; + } + //-------------------------------------------------------------------------- + // Dispatch + // + // Unscoped dispatch, will dispatch to all unscoped subscribers + // Use GetDispatcher for scoped dispatching + // + //-------------------------------------------------------------------------- + public static void Dispatch(params object[] args) where MessageType : System.Delegate => instance.Internal_DispatchEvent(typeof(MessageType).GetHashCode(), args); + public static void Dispatch(System.Type messageType, params object[] args) => instance.Internal_DispatchEvent(messageType.GetHashCode(), args); + private void Internal_DispatchEvent(int type, params object[] args) + { + if(!m_scopes.ContainsKey(""))Internal_GetDispatcher(""); + m_scopes[""].Dispatch(type, args); + } + + //-------------------------------------------------------------------------- + // Subscribe + //-------------------------------------------------------------------------- + public static void Subscribe(MessageType subscriber) where MessageType : System.Delegate => instance.Internal_Subscribe("", subscriber); + public static void Subscribe(string scope, MessageType subscriber) where MessageType : System.Delegate => instance.Internal_Subscribe(scope, subscriber); + private void Internal_Subscribe(string scope, MessageType subscriber) where MessageType : System.Delegate + { + if(!m_scopes.ContainsKey(scope))Internal_GetDispatcher(scope); + m_scopes[scope].Subscribe(subscriber); + } + + //-------------------------------------------------------------------------- + // Unsubscribe + //-------------------------------------------------------------------------- + public static void Unsubscribe(MessageType subscriber) where MessageType : System.Delegate => instance.Internal_Unsubscribe("", subscriber); + public static void Unsubscribe(string scope, MessageType subscriber) where MessageType : System.Delegate => instance.Internal_Unsubscribe(scope, subscriber); + private void Internal_Unsubscribe(string scope, MessageType subscriber) where MessageType : System.Delegate + { + if(!m_scopes.ContainsKey(scope))return ; + m_scopes[scope].Unsubscribe(subscriber); + } +} + +//------------------------------------------------------------------------------ +// IMessageDispatcher +// +// Interface for the message dispatcher, handy for making proxy dispatchers. +// +//------------------------------------------------------------------------------ +public interface IMessageDispatcher +{ + public void Dispatch(params object[] args) where MessageType : System.Delegate; + public void Dispatch(System.Type messageType, params object[] args); + public void Subscribe(MessageType subscriber) where MessageType : System.Delegate; + public void Unsubscribe(MessageType subscriber) where MessageType : System.Delegate; +} + +//------------------------------------------------------------------------------ +// MessageDispatcher +// +// The message dispatcher is used to dispatch messages to the correct handlers. +// +//------------------------------------------------------------------------------ +public class MessageDispatcher : IMessageDispatcher +{ + //-------------------------------------------------------------------------- + // Properties + public string Scope {get; private set;} + + //-------------------------------------------------------------------------- + // Subscribers + private Dictionary> m_subscribers = new(); + + //-------------------------------------------------------------------------- + // Cache + private List m_delegateList = new(8); + + //-------------------------------------------------------------------------- + // Constructor + //-------------------------------------------------------------------------- + public MessageDispatcher(string scope) + { + Scope = scope; + } + + //-------------------------------------------------------------------------- + // Subscribe + //-------------------------------------------------------------------------- + public void Subscribe(MessageType subscriber) where MessageType : System.Delegate + { + // Find message type set + int type = subscriber.GetType().GetHashCode(); + if(!m_subscribers.TryGetValue(type, out var messageSet)) + { + // First time we see this message type, create a new set + messageSet = new HashSet(); + m_subscribers[type] = messageSet; + } + + // Add listener + Debug.Assert(!messageSet.Contains(subscriber as System.Delegate), "Listener added twice! [" + subscriber.GetType() + "]"); + messageSet.Add(subscriber as System.Delegate); + } + + //-------------------------------------------------------------------------- + // Unsubscribe + //-------------------------------------------------------------------------- + public void Unsubscribe(MessageType subscriber) where MessageType : System.Delegate + { + // Find message type set + int type = subscriber.GetType().GetHashCode(); + if(m_subscribers.TryGetValue(type, out var messageSet)) + { + messageSet.Remove(subscriber as System.Delegate); + if(messageSet.Count == 0) + m_subscribers.Remove(type); + } + } + + //-------------------------------------------------------------------------- + // DispatchEvent + //-------------------------------------------------------------------------- + public void Dispatch(params object[] args) where MessageType : System.Delegate => Internal_DispatchEvent(typeof(MessageType).GetHashCode(), args); + public void Dispatch(System.Type messageType, params object[] args) => Internal_DispatchEvent(messageType.GetHashCode(), args); + public void Dispatch(int type, params object[] args) => Internal_DispatchEvent(type, args); + private void Internal_DispatchEvent(int type, params object[] args) + { + if(m_subscribers.TryGetValue(type, out var messageSet)) + { + if(messageSet.Count > 0) + { + m_delegateList.Clear(); + foreach(System.Delegate dg in messageSet)m_delegateList.Add(dg); + + for(int i = 0; i < m_delegateList.Count; i++) + { + try + { + m_delegateList[i].Method.Invoke(m_delegateList[i].Target, args); + } + catch(System.Exception e) + { + Debug.Assert(false, "Error dispatching message: " + e.Message); + } + } + } + } + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..6a7ff66 --- /dev/null +++ b/Program.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// MIT License +// +// Copyright (c) 2024 Tobias Barendt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//------------------------------------------------------------------------------ + +Example example = new Example(); + diff --git a/README.md b/README.md index 6644cb7..6fc3065 100644 --- a/README.md +++ b/README.md @@ -1 +1,85 @@ -# MessageBus \ No newline at end of file +# MessageBus +A C# scoped message bus system that is lightweight, type safe and easy to use + +--- + +## Install +This is a single file library, place MessageBus/MessageBus.cs any where in your project + + +--- +## Usage without scope +Step #1 +Declare your message types as delegates + +``` +public MyMessages +{ + public delegate void LogText(string text); +} +``` +> The delegates can be in any class + +Step #2 +Create a function that will listen to the message + +``` +public MyListener +{ + private void OnLogText(string text) + { + Console.WriteLine("Log " + text); + } +} +``` + +Step #3 +Subscribe to the message + +``` + +public MyListener +{ + public MyListener() + { + MessageBus.Subscribe(OnLogText); + } + + private void OnLogText(string text) + { + Console.WriteLine("Log " + text); + } +} + +``` + +Step #4 +Dispatch messages +``` +MessageBus.Dispatch("Hello world!"); + +``` +--- +## Usage with scope +To subscribe to a scoped message bus, use a string for the scope +``` +MessageBus.Subscribe("scopeName", OnLogText); +``` + +To dispatch a message to a scooped bus + +``` +var dispatcher = MessageBus.GetDispatcher("scopeName"); +dispatcher.Dispatch("Hello scoped world!"); +``` +> The MessageDispatcher can be used for dependency injection so the dispatcher fo messages doesn't need to know the scope + + + + + + + + + +