https://github.com/annulusgames/zeromessenger
Zero-allocation, extremely fast in-memory messaging library for .NET and Unity.
https://github.com/annulusgames/zeromessenger
Last synced: 9 months ago
JSON representation
Zero-allocation, extremely fast in-memory messaging library for .NET and Unity.
- Host: GitHub
- URL: https://github.com/annulusgames/zeromessenger
- Owner: annulusgames
- License: mit
- Created: 2024-10-28T05:46:11.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2025-02-17T14:24:10.000Z (11 months ago)
- Last Synced: 2025-04-06T07:10:04.768Z (9 months ago)
- Language: C#
- Size: 696 KB
- Stars: 127
- Watchers: 1
- Forks: 4
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Zero Messenger
Zero-allocation, extremely fast in-memory messaging library for .NET and Unity.
[](https://www.nuget.org/packages/ZeroMessenger)
[](https://github.com/AnnulusGames/ZeroMessenger/releases)
[](LICENSE)
English | [日本語](README_JA.md)
## Overview
Zero Messenger is a high-performance messaging library for .NET and Unity. It provides `MessageBroker` as an easy-to-use event system for subscribing and unsubscribing, as well as support for implementing the Pub/Sub pattern with `IMessagePublisher/IMessageSubscriber`.
Zero Messenger is designed with performance as a priority, achieving faster `Publish()` operations than libraries such as [MessagePipe](https://github.com/Cysharp/MessagePipe) and [VitalRouter](https://github.com/hadashiA/VitalRouter). Moreover, there are no allocations during publishing.


Additionally, it minimizes allocations when constructing message pipelines compared to other libraries. Below is a benchmark result from executing `Subscribe/Dispose` 10,000 times.

## Installation
### NuGet Packages
Zero Messenger requires .NET Standard 2.1 or later. The package is available on NuGet.
### .NET CLI
```ps1
dotnet add package ZeroMessenger
```
### Package Manager
```ps1
Install-Package ZeroMessenger
```
### Unity
You can use Zero Messenger in Unity by utilizing NuGetForUnity. For more details, see the [Unity](#unity-1) section.
## Quick Start
You can easily implement global Pub/Sub using `MessageBroker.Default`.
```cs
using System;
using ZeroMessenger;
// Subscribe to messages
var subscription = MessageBroker.Default.Subscribe(x =>
{
Console.WriteLine(x.Text);
});
// Publish a message
MessageBroker.Default.Publish(new Message("Hello!"));
// Unsubscribe
subscription.Dispose();
// Type used for the message
public record struct Message(string Text) { }
```
Additionally, an instance of `MessageBroker` can be used similarly to `event` or Rx's `Subject`.
```cs
var broker = new MessageBroker();
broker.Subscribe(x =>
{
Console.WriteLine(x);
});
broker.Publish(10);
broker.Dispose();
```
## Dependency Injection
By adding Zero Messenger to a DI container, you can easily implement Pub/Sub between services.
Zero Messenger supports Pub/Sub on `Microsoft.Extensions.DependencyInjection`, which requires the [ZeroMessenger.DependencyInjection](https://www.nuget.org/packages/ZeroMessenger.DependencyInjection/) package.
#### .NET CLI
```ps1
dotnet add package ZeroMessenger.DependencyInjection
```
#### Package Manager
```ps1
Install-Package ZeroMessenger.DependencyInjection
```
### Generic Host
Adding `services.AddZeroMessenger()` registers Zero Messenger in `IServiceCollection`. The following example demonstrates Pub/Sub implementation on a Generic Host.
```cs
using ZeroMessenger;
using ZeroMessenger.DependencyInjection;
Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
// Add Zero Messenger
services.AddZeroMessenger();
services.AddSingleton();
services.AddSingleton();
})
.Build()
.Run();
public record struct Message(string Text) { }
public class ServiceA
{
IMessagePublisher publisher;
public ServiceA(IMessagePublisher publisher)
{
this.publisher = publisher;
}
public Task SendAsync(CancellationToken cancellationToken = default)
{
publisher.Publish(new Message("Hello!"));
}
}
public class ServiceB : IDisposable
{
IDisposable subscription;
public ServiceB(IMessageSubscriber subscriber)
{
subscription = subscriber.Subscribe(x =>
{
Console.WriteLine(x);
});
}
public void Dispose()
{
subscription.Dispose();
}
}
```
## Publisher/Subscriber
The interfaces used for Pub/Sub are `IMessagePublisher` and `IMessageSubscriber`. The `MessageBroker` implements both of these interfaces.
```cs
public interface IMessagePublisher
{
void Publish(T message, CancellationToken cancellationToken = default);
ValueTask PublishAsync(T message, AsyncPublishStrategy publishStrategy = AsyncPublishStrategy.Parallel, CancellationToken cancellationToken = default);
}
public interface IMessageSubscriber
{
IDisposable Subscribe(MessageHandler handler);
IDisposable SubscribeAwait(AsyncMessageHandler handler, AsyncSubscribeStrategy subscribeStrategy = AsyncSubscribeStrategy.Sequential);
}
```
### IMessagePublisher
`IMessagePublisher` is an interface for publishing messages. You can publish messages using `Publish()`, and with `PublishAsync()`, you can wait for all processing to complete.
```cs
IMessagePublisher publisher;
// Publish a message (Fire-and-forget)
publisher.Publish(new Message("Foo!"));
// Publish a message and wait for all subscribers to finish processing
await publisher.PublishAsync(new Message("Bar!"), AsyncPublishStrategy.Parallel, cancellationToken);
```
You can specify `AsyncPublishStrategy` to change how asynchronous message handlers are handled.
| `AsyncPublishStrategy` | - |
| --------------------------------- | -------------------------------------------------------------------------- |
| `AsyncPublishStrategy.Parallel` | All asynchronous message handlers are executed in parallel. |
| `AsyncPublishStrategy.Sequential` | Asynchronous message handlers are queued and executed one by one in order. |
### IMessageSubscriber
`IMessageSubscriber` is an interface for subscribing to messages. It provides an extension method `Subscribe()` that accepts an `Action`, allowing you to easily subscribe using lambda expressions. You can unsubscribe by calling `Dispose()` on the returned `IDisposable`.
```cs
IMessageSubscriber subscriber;
// Subscribe to messages
var subscription = subscriber.Subscribe(x =>
{
Console.WriteLine(x.Text);
});
// Unsubscribe
subscription.Dispose();
```
You can also perform asynchronous processing within the subscription using `SubscribeAwait()`.
```cs
var subscription = subscriber.SubscribeAwait(async (x, ct) =>
{
await FooAsync(x, ct);
}, AsyncSubscribeStrategy.Sequential);
```
By specifying `AsyncSubscribeStrategy`, you can change how messages are handled when received during processing.
| `AsyncSubscribeStrategy` | - |
| ----------------------------------- | ------------------------------------------------------------ |
| `AsyncSubscribeStrategy.Sequential` | Messages are queued and executed in order. |
| `AsyncSubscribeStrategy.Parallel` | Messages are executed in parallel. |
| `AsyncSubscribeStrategy.Switch` | Cancels the ongoing processing and executes the new message. |
| `AsyncSubscribeStrategy.Drop` | Ignores new messages during ongoing processing. |
## Filter
Filters allow you to add processing before and after message handling.
### Creating a Filter
To create a new filter, define a class that implements `IMessageFilter`.
```cs
public class NopFilter : IMessageFilter
{
public async ValueTask InvokeAsync(T message, CancellationToken cancellationToken, Func next)
{
try
{
// Call the next processing step
await next(message, cancellationToken);
}
catch
{
throw;
}
finally
{
}
}
}
```
The definition of `IMessageFilter` adopts the async decorator pattern, which is also used in [ASP.NET Core middleware](https://learn.microsoft.com/ja-jp/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0).
Here’s an example of a filter that adds logging before and after processing.
```cs
public class LoggingFilter : IMessageFilter
{
public async ValueTask InvokeAsync(T message, CancellationToken cancellationToken, Func next)
{
Console.WriteLine("Before");
await next(message, cancellationToken);
Console.WriteLine("After");
}
}
```
### Adding Filters
There are several ways to add a created filter.
If adding directly to `MessageBroker`, use `AddFilter()`. The order of filter application will follow the order of addition.
```cs
var broker = new MessageBroker();
// Add a filter
broker.AddFilter>();
```
To add a global filter to a publisher in the DI container, configure it within the `AddZeroMessenger()` method.
```cs
Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
services.AddZeroMessenger(messenger =>
{
// Specify the type to add
messenger.AddFilter>();
// Add with open generics
messenger.AddFilter(typeof(LoggingFilter<>));
});
})
.Build()
.Run();
```
To add individual filters when subscribing, you can use the `WithFilter() / WithFilters()` extension methods.
```cs
IMessageSubscriber subscriber;
subscriber
.WithFilter>()
.Subscribe(x =>
{
});
```
### PredicateFilter
Zero Messenger provides `PredicateFilter`. When you pass a `Predicate` as an argument to `AddFilter()` or `WithFilter()`, a `PredicateFilter` created based on that predicate is automatically added.
```cs
public record struct FooMessage(int Value);
IMessageSubscriber subscriber;
subscriber
.WithFilter(x => x.Value >= 0) // Exclude values less than 0
.Subscribe(x =>
{
});
```
## R3
Zero Messenger supports integration with [Cysharp/R3](https://github.com/Cysharp/R3). To enable this feature, add the `ZeroMessenger.R3` package.
### .NET CLI
```ps1
dotnet add package ZeroMessenger.R3
```
### Package Manager
```ps1
Install-Package ZeroMessenger.R3
```
By adding ZeroMessenger.R3, you gain access to operators for converting `IMessageSubscriber` to `Observable` and connecting `Observable` to `IMessagePublisher`.
```cs
// Convert IMessageSubscriber to Observable
subscriber.ToObservable()
.Subscribe(x => { });
// Subscribe to Observable and convert it to IMessagePublisher's Publish()
observable.SubscribeToPublish(publisher);
// SubscribeAwait to Observable and convert it to IMessagePublisher's PublishAsync()
observable.SubscribeAwaitToPublish(publisher, AwaitOperation.Sequential, AsyncPublishStrategy.Parallel);
```
## Unity
You can use Zero Messenger in Unity by installing NuGet packages via NugetForUnity.
### Requirements
* Unity 2021.3 or later
### Installation
1. Install [NugetForUnity](https://github.com/GlitchEnzo/NuGetForUnity).
2. Open the NuGet window by selecting `NuGet > Manage NuGet Packages`, search for the `ZeroMessenger` package, and install it.

### VContainer
There is also an extension package available for handling Zero Messenger with VContainer's DI container.
To install ZeroMessenger.VContainer, open the Package Manager window by selecting `Window > Package Manager`, then use `[+] > Add package from git URL` and enter the following URL:
```plaintext
https://github.com/AnnulusGames/ZeroMessenger.git?path=src/ZeroMessenger.Unity/Assets/ZeroMessenger.VContainer
```
By introducing ZeroMessenger.VContainer, the `IContainerBuilder` gains the `AddZeroMessenger()` extension method. Calling this method adds Zero Messenger to the DI container, allowing `IMessagePublisher` and `IMessageSubscriber` to be injected.
```cs
using VContainer;
using VContainer.Unity;
using ZeroMessenger.VContainer;
public class ExampleLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
// Add Zero Messenger
builder.AddZeroMessenger();
}
}
```
> [!NOTE]
> `AddZeroMessenger()` registers using Open Generics, which may not work with IL2CPP versions prior to Unity 2022.1.
## License
This library is released under the [MIT License](LICENSE).