MidR 6.1.0
dotnet add package MidR --version 6.1.0
NuGet\Install-Package MidR -Version 6.1.0
<PackageReference Include="MidR" Version="6.1.0" />
<PackageVersion Include="MidR" Version="6.1.0" />
<PackageReference Include="MidR" />
paket add MidR --version 6.1.0
#r "nuget: MidR, 6.1.0"
#:package MidR@6.1.0
#addin nuget:?package=MidR&version=6.1.0
#tool nuget:?package=MidR&version=6.1.0
<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,PublishToBusAsyncitself starts awaiting once the channel is full, instead of failing or dropping — the caller feels the backpressure directly. PickDropOldest/DropNewest/DropWriteinstead 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
PublishToBusAsyncwhen the calling request should not wait for side effects (emails, audit logs, enrichment jobs). UsePublishAsyncwhen 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 —PublishAsyncwithout 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 (insideAddMidR). - Publishing with a key that has no registered handler throws a descriptive
InvalidOperationExceptionat runtime. PublishAsyncwithout aRoutingKeyis 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 andtypeof(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 | Versions 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. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Logging (>= 10.0.0)
-
net6.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 6.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 6.0.0)
- Microsoft.Extensions.Logging (>= 6.0.0)
-
net7.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 7.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 7.0.0)
- Microsoft.Extensions.Logging (>= 7.0.0)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging (>= 8.0.0)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Logging (>= 9.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.