MessagePipe Documentation

repository·master·Indexed 23 days ago

https://github.com/cysharp/messagepipe

A high-performance in-memory and distributed messaging pipeline for .NET and Unity. It supports various messaging patterns including Pub/Sub (sync, async, keyed, and buffered), Mediator (CQRS) via request/response handlers, and Interprocess Communication (IPC). Built on Microsoft.Extensions.DependencyInjection, it includes tools for subscription lifecycle management via DisposableBag, middleware-style Filters, and diagnostics to prevent memory leaks.

Tokens
8.5K
Snippets
18
Records
27
Agent score
34%

What's inside MessagePipe

  1. Control Lifetime with Singleton and Scoped interfaces

    master

    By default, the lifetime of IPublisher<T> and ISubscriber<T> is determined by MessagePipeOptions.InstanceLifetime.

    However, you can explicitly request specific lifetimes by using these interfaces:

    • ISingletonPublisher<TMessage> / ISingletonSubscriber<TKey, TMessage>: Always uses Singleton lifetime.
    • IScopedPublisher<TMessage> / IScopedSubscriber<TKey, TMessage>: Uses Scoped lifetime.
  2. Implement and apply Filters

    master

    Filters implement the Middleware pattern, allowing you to hook into the invocation pipeline before and after a message is handled.

    Filter Types

    • MessageHandlerFilter<T> (Sync)
    • AsyncMessageHandlerFilter<T> (Async)
    • RequestHandlerFilter<TReq, TRes> (Request)
    • AsyncRequestHandlerFilter<TReq, TRes> (Async Request)

    Where to apply Filters

    1. Global: Via MessagePipeOptions.AddGlobalMessageHandlerFilter during setup.
    2. Per Handler Type: Using the [MessageHandlerFilter(type, order)] attribute on a handler class.
    3. Per Subscription: Passing filter instances directly into the Subscribe method.

    Filters are sorted by their Order property.

    // 1. Define a custom filter
    public class ChangedValueFilter<T> : MessageHandlerFilter<T>
    {
        T lastValue;
        public override void Handle(T message, Action<T> next)
        {
            if (EqualityComparer<T>.Default.Equals(message, lastValue)) return;
            lastValue = message;
            next(message);
        }
    }
    
    // 2. Apply per subscription
    subscriber.Subscribe(x => Console.WriteLine(x), new ChangedValueFilter<int>(){ Order = 100 });
    
    // 3. Apply per handler type using attributes
    [MessageHandlerFilter(typeof(ChangedValueFilter<>), 100)]
    public class WriteLineHandler<T> : IMessageHandler<T>
    {
        public void Handle(T message) => Console.WriteLine(message);
    }
    
    // 4. Apply globally during DI configuration
    services.AddMessagePipe(options =>
    {
        options.AddGlobalMessageHandlerFilter(typeof(ChangedValueFilter<>), 100);
    });
  3. Use Keyed (Topic-based) Publish/Subscribe

    master
    If you need to distribute messages based on specific IDs (topics) rather than just the message type, use the keyed interfaces: IPublisher<TKey, TMessage> and ISubscriber<TKey, TMessage>. This is useful for scenarios like routing messages to specific user connections or browser sessions.
  4. Implement the Mediator pattern with Request/Response/All

    master

    MessagePipe supports the Mediator pattern (similar to MediatR) through request/response handlers. You can define a request type and a corresponding handler that processes it.

    • Single Handler: Use IRequestHandler<TRequest, TResponse> for a 1-to-1 mapping.
    • Multiple Handlers: Use IRequestAllHandler<TRequest, TResponse> or IAsyncRequestAllHandler<TRequest, TResponse> to invoke all registered handlers for a single request.

    Handlers are typically resolved via Dependency Injection.

  5. Monitor subscription leaks with MessagePipeDiagnosticsInfo

    master

    You can monitor the number of active subscriptions using MessagePipeDiagnosticsInfo, which can be retrieved from your Service Provider or DI container.

    Key properties:

    • SubscribeCount: The current number of active subscriptions.
    • GetCapturedStackTraces(): Returns stack traces for all subscriptions (requires MessagePipeOptions.EnableCaptureStackTrace to be enabled).
    • GetGroupedByCaller(): Groups stack traces by the caller of the subscribe method (requires MessagePipeOptions.EnableCaptureStackTrace to be enabled).

    In Unity, you can use the Window -> MessagePipe Diagnostics window to visualize this information.

  6. How AsyncPublishStrategy works

    master

    The AsyncPublishStrategy determines how IAsyncPublisher.PublishAsync handles multiple subscribers:

    • Parallel: Uses Task.WhenAll to execute subscribers concurrently (Default).
    • Sequential: Awaits each subscriber one by one.

    This strategy can be set globally via MessagePipeOptions.DefaultAsyncPublishStrategy or passed explicitly to the PublishAsync method call.

  7. Perform IPC-RPC with MessagePipe.Interprocess

    master

    You can perform Remote Procedure Calls (RPC) across processes using IRemoteRequestHandler<TRequest, TResponse> on the server side and IRemoteRequestHandler<TRequest, TResponse> on the client side. This requires TcpInterprocess or NamedPipeInterprocess to be enabled.

    Server Configuration

    Set HostAsServer = true in the interprocess options.

    Implementation Example

    Server Handler:

    public class MyAsyncHandler : IAsyncRequestHandler<int, string>
    {
        public async ValueTask<string> InvokeAsync(int request, CancellationToken cancellationToken = default)
        {
            await Task.Delay(1);
            return request == -1 ? throw new Exception("NO -1") : $"ECHO:{request}";
        }
    }

    Client Call:

    async void A(IRemoteRequestHandler<int, string> remoteHandler)
    {
        var v = await remoteHandler.InvokeAsync(9999);
        Console.WriteLine(v); // ECHO:9999
    }
    Host.CreateDefaultBuilder()
        .ConfigureServices((ctx, services) => {
            services.AddMessagePipe()
                .AddTcpInterprocess("127.0.0.1", 3215, x => {
                    x.HostAsServer = true;
                });
        });
  8. Enable MessagePipe Diagnostics window in Unity

    master

    To use the visual diagnostics window in Unity, you must register the service provider with GlobalMessagePipe during your application's startup.

    VContainer Example

    public class MessagePipeDemo : VContainer.Unity.IStartable
    {
        public MessagePipeDemo(IObjectResolver resolver)
        {
            GlobalMessagePipe.SetProvider(resolver.AsServiceProvider());
        }
    }

    Zenject Example

    void Configure(DiContainer container)
    {
        GlobalMessagePipe.SetProvider(container.AsServiceProvider());
    }

    Built-in DI Example

    var provider = builder.BuildServiceProvider();
    GlobalMessagePipe.SetProvider(provider);
  9. Use EventFactory to create C#-like events

    master

    The EventFactory allows you to create generic publishers and subscribers (IPublisher/ISubscriber, IAsyncPublisher/IAsyncSubscriber, etc.) that behave like C# events but are tied to specific instances rather than grouped by type.

    Key advantages over standard C# events:

    • Use Subscribe/Dispose for easier subscription management.
    • Supports both synchronous and asynchronous patterns.
    • Supports both bufferless and buffered modes.
    • Calling Dispose() on a publisher unsubscribes all associated subscribers.
    • Supports an invocation pipeline via Filters.
    • Provides diagnostics via MessagePipeDiagnosticsInfo and leak prevention via MessagePipe.Analyzer.

    If you are working outside of Dependency Injection, you can use GlobalMessagePipe.CreateEvent<T>().

    public class BetterEvent : IDisposable
    {
        IDisposablePublisher<int> tickPublisher;
        public ISubscriber<int> OnTick { get; }
    
        public BetterEvent(EventFactory eventFactory)
        {
            // CreateEvent returns a tuple containing the publisher and the subscriber
            (tickPublisher, OnTick) = eventFactory.CreateEvent<int>();
        }
    
        int count;
        void Tick()
        {
            tickPublisher.Publish(count++);
        }
    
        public void Dispose()
        {
            // Unsubscribes all from the publisher
            tickPublisher.Dispose();
        }
    }
  10. Use GlobalMessagePipe for simplified access

    master

    If you are using BuiltinContainerBuilder, it does not support scopes (it is always InstanceScope.Singleton), IRequestAllHandler/IAsyncRequestAllHandler, or many other DI functionalities. In these cases, it is recommended to use GlobalMessagePipe to access publishers and subscribers.

    To use this pattern:

    1. Build your service provider.
    2. Set it using GlobalMessagePipe.SetProvider(provider).
    3. Access publishers and subscribers via GlobalMessagePipe.GetPublisher<T>() and GlobalMessagePipe.GetSubscriber<T>().
    // create provider and set to Global(to enable diagnostics window and global fucntion)
    var provider = builder.BuildServiceProvider();
    GlobalMessagePipe.SetProvider(provider);
    
    // --- to use MessagePipe, you can use from GlobalMessagePipe.
    var p = GlobalMessagePipe.GetPublisher<int>();
    var s = GlobalMessagePipe.GetSubscriber<int>();
    
    var d = s.Subscribe(x => Debug.Log(x));
    
    p.Publish(10);
    p.Publish(20);
    p.Publish(30);
    
    d.Dispose();
  11. Configure MessagePipe in .NET Generic Host

    master

    MessagePipe is built on top of Microsoft.Extensions.DependencyInjection. To use it in a .NET Generic Host (such as ASP.NET Core, MAUI, or ConsoleAppFramework), call AddMessagePipe() within the ConfigureServices method.

    You can also pass an options action to AddMessagePipe(options => { ... }) to configure specific settings.

    using MessagePipe;
    using Microsoft.Extensions.DependencyInjection;
    
    Host.CreateDefaultBuilder()
        .ConfigureServices((ctx, services) =>
        {
            services.AddMessagePipe(); // AddMessagePipe(options => { }) for configure options
        })
  12. Use GlobalMessagePipe for global access

    master

    If you need to access publishers, subscribers, or handlers from a global scope (outside of standard DI resolution), you can use the GlobalMessagePipe static helper.

    To enable this, you must call GlobalMessagePipe.SetProvider(host.Services) after building your host.

    var host = Host.CreateDefaultBuilder()
        .ConfigureServices((ctx, x) =>
        {
            x.AddMessagePipe();
        })
        .Build();
    
    GlobalMessagePipe.SetProvider(host.Services);
    
    await host.RunAsync();
    
    // Usage elsewhere
    var publisher = GlobalMessagePipe.GetPublisher<MyMessage>();