Table of Contents

Mediator — Mediator

An allocation-light mediator that coordinates messages between loosely coupled components. It supports:

  • Commands (request/response) with ValueTask
  • Notifications (fan-out) with ValueTask
  • Streaming commands via IAsyncEnumerable (netstandard2.1+/netcoreapp3.0+)
  • Global behaviors: Pre, Post, and Whole (around) with minimal overhead
  • Sync adapters for convenience

The mediator is immutable and thread-safe after Build(). Builders are mutable and not thread-safe.


Quick start

using PatternKit.Behavioral.Mediator;

var mediator = Mediator.Create()
    // behaviors
    .Pre(static (in object req, CancellationToken _) => { Console.WriteLine($"pre:{req.GetType().Name}"); return default; })
    .Whole(static (in object req, CancellationToken ct, Mediator.MediatorNext next) => next(in req, ct))
    .Post(static (in object req, object? res, CancellationToken _) => { Console.WriteLine($"post:{res}"); return default; })

    // command: Ping -> string
    .Command<Ping, string>(static (in Ping p, CancellationToken _) => new ValueTask<string>("pong:" + p.Value))

    // notification: Note
    .Notification<Note>(static (in Note n, CancellationToken _) => { Console.WriteLine(n.Text); return default; })

#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
    // streaming command
    .Stream<RangeRequest, int>(static (in RangeRequest r, CancellationToken _) => Range(r.Start, r.Count))
#endif

    .Build();

var s = await mediator.Send<Ping, string>(new Ping(5)); // "pong:5"
await mediator.Publish(new Note("hello"));

#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
await foreach (var i in mediator.Stream<RangeRequest, int>(new RangeRequest(2, 3)))
    Console.WriteLine(i); // 2, 3, 4
#endif

public readonly record struct Ping(int Value);
public readonly record struct Note(string Text);
public readonly record struct RangeRequest(int Start, int Count);
#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
static async IAsyncEnumerable<int> Range(int start, int count)
{
    for (int i = 0; i < count; i++) { await Task.Yield(); yield return start + i; }
}
#endif

Behaviors

  • Pre(in object request, CancellationToken): runs before handler execution.
  • Whole(in object request, CancellationToken, MediatorNext next): wraps the handler; can short-circuit or decorate.
  • Post(in object request, object? response, CancellationToken): runs after handler completion.

Notifications are not wrapped by Whole behaviors (fire-and-forget fan-out semantics), but Pre/Post still run.


API (at a glance)

public sealed class Mediator
{
    // Build
    public static Mediator.Builder Create();

    // Use
    public ValueTask<TResponse> Send<TRequest, TResponse>(in TRequest request, CancellationToken ct = default);
    public ValueTask Publish<TNotification>(in TNotification notification, CancellationToken ct = default);
#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
    public IAsyncEnumerable<TItem> Stream<TRequest, TItem>(in TRequest request, CancellationToken ct = default);
#endif

    public sealed class Builder
    {
        // Behaviors
        public Builder Pre(PreBehavior behavior);
        public Builder Post(PostBehavior behavior);
        public Builder Whole(WholeBehavior behavior);

        // Commands
        public Builder Command<TRequest, TResponse>(CommandHandlerTyped<TRequest, TResponse> handler);
        public Builder Command<TRequest, TResponse>(SyncCommandHandlerTyped<TRequest, TResponse> handler);

        // Notifications
        public Builder Notification<TNotification>(NotificationHandlerTyped<TNotification> handler);
        public Builder Notification<TNotification>(SyncNotificationHandlerTyped<TNotification> handler);

        // Streams (netstandard2.1+/netcoreapp3.0+)
        public Builder Stream<TRequest, TItem>(StreamHandlerTyped<TRequest, TItem> handler);

        public Mediator Build();
    }
}

Design notes

  • Typed delegates adapt to object-typed internal handlers once at Build time to keep hot paths tight.
  • ValueTask everywhere; sync adapters avoid allocation when the work completes synchronously.
  • Streaming requires netstandard2.1+/netcoreapp3.0+; the build conditionally includes the feature.

Error behavior

  • Send throws if no command handler is registered for the request type or if the handler returns an incompatible result type.
  • Publish without handlers is a no-op.
  • Stream throws if no stream handler is registered for the request type.

Testing

See PatternKit.Tests/Behavioral/Mediator/MediatorTests.cs for TinyBDD scenarios covering Send, Publish, and Stream with behaviors.