Zero Messenger

repository·main·Indexed 41 days ago

https://github.com/annulusgames/zeromessenger

A high-performance, zero-allocation in-memory messaging library for .NET and Unity designed for fast Pub/Sub operations. Features include MessageBroker<T>, IMessagePublisher<T>, IMessageSubscriber<T>, and a middleware-style filter system via IMessageFilter<T>. Supports integration with Microsoft.Extensions.DependencyInjection, R3, and VContainer.

Tokens
2.7K
Snippets
10
Records
14
Agent score
2%

What's inside Zero Messenger

  1. How IMessageFilter<T> works

    main

    Filters allow you to intercept message processing before and after the handler executes. They follow an async decorator pattern similar to ASP.NET Core middleware. Implement IMessageFilter<T> and use the next delegate to pass the message to the next step in the pipeline.

    public class LoggingFilter<T> : IMessageFilter<T>
    {
        public async ValueTask InvokeAsync(T message, CancellationToken cancellationToken, Func<T, CancellationToken, ValueTask> next)
        {
            Console.WriteLine("Before");
            await next(message, cancellationToken);
            Console.WriteLine("After");
        }
    }
  2. Integrate Zero Messenger with VContainer in Unity

    main

    Zero Messenger provides an extension package for Unity users utilizing the VContainer dependency injection framework. This allows you to inject IMessagePublisher<T> and IMessageSubscriber<T> directly into your classes.

    Installation

    1. Open the Unity Package Manager (Window > Package Manager).
    2. Click the [+] button and select Add package from git URL.
    3. Enter the following URL: https://github.com/AnnulusGames/ZeroMessenger.git?path=src/ZeroMessenger.Unity/Assets/ZeroMessenger.VContainer

    Configuration

    Use the AddZeroMessenger() extension method on your IContainerBuilder within a LifetimeScope to register the messenger services.

    NOTE

    AddZeroMessenger() uses Open Generics. This may cause issues with IL2CPP on Unity versions older than 2022.1.

    using VContainer;
    using VContainer.Unity;
    using ZeroMessenger.VContainer;
    
    public class ExampleLifetimeScope : LifetimeScope
    {
        protected override void Configure(IContainerBuilder builder)
        {
            // Add Zero Messenger to the DI container
            builder.AddZeroMessenger();
        }
    }
  3. Integrate Zero Messenger with R3

    main

    By adding the ZeroMessenger.R3 package, you can convert between Zero Messenger interfaces and R3 Observables. This allows you to use R3 operators on message streams or pipe Observable data back into a publisher.

    // Install package: dotnet add package ZeroMessenger.R3
    
    // Convert IMessageSubscriber<T> to Observable<T>
    subscriber.ToObservable()
        .Subscribe(x => { });
    
    // Connect Observable<T> to IMessagePublisher<T>
    observable.SubscribeToPublish(publisher);
    
    // Connect Observable<T> to IMessagePublisher<T>.PublishAsync()
    observable.SubscribeAwaitToPublish(publisher, AwaitOperation.Sequential, AsyncPublishStrategy.Parallel);
  4. Integrate Zero Messenger with Dependency Injection

    main

    To use Zero Messenger with Microsoft.Extensions.DependencyInjection, install the ZeroMessenger.DependencyInjection package. Use services.AddZeroMessenger() to register the library in your IServiceCollection.

    # .NET CLI
    dotnet add package ZeroMessenger.DependencyInjection
    
    # Package Manager
    Install-Package ZeroMessenger.DependencyInjection
    Host.CreateDefaultBuilder()
        .ConfigureServices((context, services) =>
        {
            // Add Zero Messenger
            services.AddZeroMessenger();
    
            services.AddSingleton<ServiceA>();
            services.AddSingleton<ServiceB>();
        })
        .Build()
        .Run();
  5. Add filters to the message pipeline

    main

    You can add filters in three ways:

    1. Directly to a MessageBroker<T>: Use AddFilter<T>(). Filters are applied in the order they are added.
    2. Globally via Dependency Injection: Configure filters within AddZeroMessenger() during service registration.
    3. Per-subscription: Use the WithFilter<T>() or WithFilters() extension methods on an IMessageSubscriber<T>.
    // 1. To a broker
    var broker = new MessageBroker<int>();
    broker.AddFilter<LoggingFilter<int>>();
    
    // 2. Globally in DI
    services.AddZeroMessenger(messenger =>
    {
        messenger.AddFilter<LoggingFilter<Message>>();
        messenger.AddFilter(typeof(LoggingFilter<>));
    });
    
    // 3. Per subscription
    subscriber
        .WithFilter<LoggingFilter<Message>()}
        .Subscribe(x => { });
  6. Quick Start with MessageBroker<T>

    main

    You can use MessageBroker<T>.Default for a global Pub/Sub implementation, or instantiate a local MessageBroker<T> to act like an event or an Rx Subject<T>.

    using System;
    using ZeroMessenger;
    
    // Subscribe to messages
    var subscription = MessageBroker<Message>.Default.Subscribe(x =>
    {
        Console.WriteLine(x.Text);
    });
    
    // Publish a message
    MessageBroker<Message>.Default.Publish(new Message("Hello!"));
    
    // Unsubscribe
    subscription.Dispose();
    
    // Type used for the message
    public record struct Message(string Text) { }
  7. Use PredicateFilter for conditional message handling

    main

    Zero Messenger provides PredicateFilter<T>. If you pass a Predicate<T> to AddFilter<T>() or WithFilter<T>(), a PredicateFilter<T> is automatically created. This allows you to exclude messages that do not meet specific criteria.

    public record struct FooMessage(int Value);
    
    IMessageSubscriber<FooMessage> subscriber;
    
    // Exclude values less than 0
    subscriber
        .WithFilter(x => x.Value >= 0)
        .Subscribe(x => { });
  8. Use IMessageSubscriber<T> for subscribing to messages

    main

    The IMessageSubscriber<T> interface provides methods to listen for messages. Use Subscribe() for synchronous handlers (returning an IDisposable to unsubscribe) or SubscribeAwait() for asynchronous handlers.

    IMessageSubscriber<Message> subscriber;
    
    // Subscribe to messages
    var subscription = subscriber.Subscribe(x =>
    {
        Console.WriteLine(x.Text);
    });
    
    // Unsubscribe
    subscription.Dispose();
    
    // Asynchronous processing
    var asyncSubscription = subscriber.SubscribeAwait(async (x, ct) =>
    {
        await FooAsync(x, ct);
    }, AsyncSubscribeStrategy.Sequential);
  9. Use IMessagePublisher<T> for publishing messages

    main

    The IMessagePublisher<T> interface allows you to send messages. You can use Publish() for fire-and-forget operations or PublishAsync() to wait for all subscribers to finish processing.

    IMessagePublisher<Message> 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);