MidR 6.1.0

dotnet add package MidR --version 6.1.0
                    
NuGet\Install-Package MidR -Version 6.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MidR" Version="6.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MidR" Version="6.1.0" />
                    
Directory.Packages.props
<PackageReference Include="MidR" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MidR --version 6.1.0
                    
#r "nuget: MidR, 6.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MidR@6.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MidR&version=6.1.0
                    
Install as a Cake Addin
#tool nuget:?package=MidR&version=6.1.0
                    
Install as a Cake Tool

<p align="center"> <a href="https://dotnet.microsoft.com/" target="_blank"> <img src="https://upload.wikimedia.org/wikipedia/commons/e/ee/.NET_Core_Logo.svg" width="120" alt=".NET Logo" /> </a> </p>

<h1 align="center">MidR</h1>

<p align="center">

<a href="https://www.nuget.org/packages/MidR"> <img class="badge" src="https://img.shields.io/nuget/v/MidR?color=purple&label=MidR" alt="MidR NuGet version" /> </a> <a href="https://www.nuget.org/packages/MidR"> <img class="badge" src="https://img.shields.io/nuget/dt/MidR?color=blue" alt="MidR NuGet downloads" /> </a>

</p>

MidR

A lightweight Mediator library for .NET with built-in support for request/response, in-process notifications, an async in-memory bus, and composable behavior pipelines.

Installation

dotnet add package MidR

Registration

Call AddMidR on your IServiceCollection. Pass the assemblies that contain your handlers — if you omit them, MidR scans all loaded assemblies automatically.

// Explicit assembly
builder.Services.AddMidR(Assembly.GetExecutingAssembly());

// Multiple assemblies
builder.Services.AddMidR(typeof(CreateOrderHandler).Assembly, typeof(UserHandler).Assembly);

// Auto-scan (no args)
builder.Services.AddMidR();

Configuring the in-memory bus

AddMidR wires up the bus behind PublishToBusAsync with an unbounded channel and Environment.ProcessorCount concurrency by default. Call .WithMemoryBus on the returned configuration to customize either.

Concurrency controls how many notifications the background dispatcher can have in-flight at once — i.e., how many are simultaneously awaiting I/O inside their handlers.

builder.Services.AddMidR(Assembly.GetExecutingAssembly())
    .WithMemoryBus(MemoryBusOptions.CreateUnbounded(maxConcurrency: 4));

The bus dispatches notifications concurrently, not in parallel. Each notification awaits its handlers cooperatively via Task.WhenAll, so the limit is about controlling pressure on downstream dependencies (database, HTTP, etc.) rather than CPU usage. Tune this value based on the connection pool size of your heaviest dependency rather than the number of cores.

Channel type defaults to unbounded — PublishToBusAsync never blocks the caller, but a producer that outruns the dispatcher for long enough can grow the queue without limit. Switch to a bounded channel to apply backpressure instead:

using System.Threading.Channels;

builder.Services.AddMidR(Assembly.GetExecutingAssembly())
    .WithMemoryBus(MemoryBusOptions.CreateBounded(
        new BoundedChannelOptions(capacity: 1000) { FullMode = BoundedChannelFullMode.Wait },
        maxConcurrency: 4));

With BoundedChannelFullMode.Wait, PublishToBusAsync itself starts awaiting once the channel is full, instead of failing or dropping — the caller feels the backpressure directly. Pick DropOldest/DropNewest/DropWrite instead if losing notifications under sustained overload is preferable to slowing the publisher down.

---

Request / Response

Defining a request

Implement IRequest<TResponse> for requests that return a value, or IRequest for void semantics (returns Unit).

public sealed record CreateOrderRequest(Guid UserId, string Product) : IRequest<CreateOrderResponse>;
public sealed record CreateOrderResponse(Guid Id);

// Void — use IRequest (backed by Unit)
public sealed record DeleteOrderRequest(Guid Id) : IRequest;

Implementing a handler

public sealed class CreateOrderHandler : IRequestHandler<CreateOrderRequest, CreateOrderResponse>
{
    public async Task<CreateOrderResponse> ExecuteAsync(
        CreateOrderRequest request,
        CancellationToken cancellationToken)
    {
        // ...
        return new CreateOrderResponse(Guid.NewGuid());
    }
}

Sending a request

Inject IMediator or ISender and call SendAsync.

public class OrdersController(IMediator mediator)
{
    public async Task<IActionResult> Create(CreateOrderRequest request)
    {
        var result = await mediator.SendAsync(request);
        return Ok(result);
    }
}

---

Notifications

Notifications decouple the publisher from one or more handlers. MidR supports two dispatch strategies.

Defining a notification

public sealed record OrderCreatedEvent(Guid OrderId, Guid UserId) : INotification;

Implementing handlers

Multiple handlers can be registered for the same notification — all of them will be invoked.

public sealed class ReserveStockHandler : INotificationHandler<OrderCreatedEvent>
{
    public async Task ExecuteAsync(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        // reserve stock...
    }
}

public sealed class SendConfirmationEmailHandler : INotificationHandler<OrderCreatedEvent>
{
    public async Task ExecuteAsync(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        // send email...
    }
}

Publishing

Inject IMediator or IPublisher.

PublishAsync — synchronous, in-process

Dispatches to all handlers sequentially and awaits their completion before returning. Use this when the caller must guarantee that all handlers have finished before continuing.

await publisher.PublishAsync(new OrderCreatedEvent(orderId, userId));
// all handlers have completed at this point
PublishToBusAsync — asynchronous, fire-and-forget

Enqueues the notification in the in-memory Channel. A background BackgroundService dequeues and dispatches each notification's handlers concurrently via Task.WhenAll, independent of the caller's lifecycle.

await publisher.PublishToBusAsync(new OrderCreatedEvent(orderId, userId));
// returns immediately; handlers run in the background

Use PublishToBusAsync when the calling request should not wait for side effects (emails, audit logs, enrichment jobs). Use PublishAsync when consistency between the main operation and its side effects is required.

---

Direct Dispatch (routing to a single handler)

By default PublishAsync fans out to every registered handler of a notification. In some scenarios — notably the Inbox Pattern in a modular monolith — an event must be delivered to exactly one owning consumer, even though several modules register a handler for the same event type. Fan-out there causes duplicate processing.

Direct Dispatch routes a notification to a single handler identified by a RoutingKey. The binding is declared on the handler with [DirectQueue("key")], and resolved at startup — the publish hot path is a single dictionary lookup.

// Define keys as shared constants so publisher and handler can't drift apart.
public static class InboxRoutes
{
    public const string Orders = "orders";
}

[DirectQueue(InboxRoutes.Orders)]
public class OrdersInboxHandler : INotificationHandler<OrderPlaced>
{
    public Task ExecuteAsync(OrderPlaced notification, CancellationToken ct) { /* ... */ }
}

// Routed to OrdersInboxHandler only — string converts implicitly to RoutingKey.
await publisher.PublishAsync(new OrderPlaced(orderId), InboxRoutes.Orders);

Semantics:

  • A handler marked [DirectQueue] is excluded from normal fan-out — PublishAsync without a key never invokes it. It is reachable only via the keyed overload.
  • Only the handler bound to the pair (TNotification, RoutingKey) runs. Direct Dispatch still flows through the notification behavior pipeline.
  • The key must be unique per notification type: two handlers bound to the same (TNotification, key) throw at startup (inside AddMidR).
  • Publishing with a key that has no registered handler throws a descriptive InvalidOperationException at runtime.
  • PublishAsync without a RoutingKey is unchanged — normal fan-out to all non-direct handlers.

---

Behaviors

Behaviors form an ordered pipeline that wraps handler execution, similar to middleware. They are ideal for cross-cutting concerns such as logging, timing, validation, and exception handling.

Request behaviors

Implement IRequestBehavior<TRequest, TResponse>. Must be an open generic type to be automatically applied to all matching requests.

public sealed class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    : IRequestBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    public async Task<TResponse> ExecuteAsync(
        TRequest request,
        RequestDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        logger.LogInformation("Handling {Request}", typeof(TRequest).Name);
        var response = await next();
        logger.LogInformation("Handled {Request}", typeof(TRequest).Name);
        return response;
    }
}

Notification behaviors

Implement INotificationBehavior<TNotification>. Also must be an open generic type.

public sealed class NotificationLoggingBehavior<TNotification>(ILogger<NotificationLoggingBehavior<TNotification>> logger)
    : INotificationBehavior<TNotification>
    where TNotification : INotification
{
    public async Task ExecuteAsync(
        TNotification notification,
        NotificationDelegate next,
        CancellationToken cancellationToken)
    {
        logger.LogInformation("Dispatching {Notification}", typeof(TNotification).Name);
        await next();
        logger.LogInformation("Dispatched {Notification}", typeof(TNotification).Name);
    }
}

Registering behaviors

Chain .WithBehaviors after AddMidR. The priority value controls execution order — lower values run first (outermost in the pipeline).

builder.Services.AddMidR(Assembly.GetExecutingAssembly())
    .WithBehaviors(config =>
    {
        // Request behaviors
        config.AddBehavior(typeof(LoggingBehavior<,>)).WithPriority(1);
        config.AddBehavior(typeof(TimingBehavior<,>)).WithPriority(2);
        config.AddBehavior(typeof(ExceptionBehavior<,>)).WithPriority(3);

        // Notification behaviors
        config.AddBehavior(typeof(NotificationLoggingBehavior<>)).WithPriority(1);
    });

**Important:** always pass the open generic definition — typeof(MyBehavior<,>) for request behaviors and typeof(MyBehavior<>) for notification behaviors. MidR closes the type for each discovered request/notification at startup.

Pipeline execution order

Given the registration above, a request flows through the pipeline as follows:

LoggingBehavior (P1)
  └─ TimingBehavior (P2)
       └─ ExceptionBehavior (P3)
            └─ Handler

Not calling next() inside a behavior short-circuits the pipeline — subsequent behaviors and the handler will not execute.

---

Unit — void requests

Unit is the return type for requests that produce no meaningful value. It avoids the need for a non-generic IRequest variant while keeping the pipeline uniform.

public sealed record DeleteUserRequest(Guid Id) : IRequest<Unit>;

public sealed class DeleteUserHandler : IRequestHandler<DeleteUserRequest, Unit>
{
    public async Task<Unit> ExecuteAsync(DeleteUserRequest request, CancellationToken cancellationToken)
    {
        // delete user...
        return Unit.Value;
    }
}

---

Interface reference

Interface Purpose
IRequest<TResponse> Marks a request that returns TResponse
IRequest Marks a void request (returns Unit)
IRequestHandler<TRequest, TResponse> Handles a specific request type
INotification Marks a notification message
INotificationHandler<TNotification> Handles a specific notification type
IMediator Combined ISender + IPublisher entry point
ISender Sends requests via SendAsync
IPublisher Publishes notifications via PublishAsync / PublishToBusAsync
IRequestBehavior<TRequest, TResponse> Pipeline behavior wrapping request handling
INotificationBehavior<TNotification> Pipeline behavior wrapping notification dispatch
Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
6.1.0 48 9/27/2026
6.0.0 138 6/16/2026
5.3.0 116 4/1/2026