Personix.Mediator.Middleware
2.0.0
dotnet add package Personix.Mediator.Middleware --version 2.0.0
NuGet\Install-Package Personix.Mediator.Middleware -Version 2.0.0
<PackageReference Include="Personix.Mediator.Middleware" Version="2.0.0" />
<PackageVersion Include="Personix.Mediator.Middleware" Version="2.0.0" />
<PackageReference Include="Personix.Mediator.Middleware" />
paket add Personix.Mediator.Middleware --version 2.0.0
#r "nuget: Personix.Mediator.Middleware, 2.0.0"
#:package Personix.Mediator.Middleware@2.0.0
#addin nuget:?package=Personix.Mediator.Middleware&version=2.0.0
#tool nuget:?package=Personix.Mediator.Middleware&version=2.0.0
Personix.Mediator.Middleware
A mediator whose pipeline steps are ordinary ASP.NET Core middleware. Send a request, it travels the real middleware chain and arrives at your endpoint method, and you get the response back — with no server, no port and no socket anywhere behind it.
Your production middleware runs unchanged. It never learns there is no server.
Install
dotnet add package Personix.Mediator.Middleware
Usage
Register the pipeline the way you register anything else:
services.AddMiddlewareMediator(cfg => cfg.Configure(app =>
{
app.UseRouting();
app.UseMiddleware<CorrelationIdMiddleware>(); // your real middleware
app.UseEndpoints(endpoints => endpoints.MapGet("/orders/{id:int}", (int id) => $"order {id}"));
}));
Then inject IMiddlewareSender and dispatch:
var response = await sender.SendAsync(SimulationHttpRequest.Get("/orders/42"));
Console.WriteLine(response.StatusCode); // 200
Console.WriteLine(response.BodyAsString()); // order 42
Configure receives a real IApplicationBuilder, so it takes the same UseMiddleware, UseRouting and MapGet calls your Program.cs does. The same configuration method can feed both a real host and this one, which means there is no second definition to drift out of step.
Writing a step
There is nothing to learn. A step is ASP.NET Core middleware, in either activation style, with no reference to this package:
// convention-activated
public sealed class CorrelationIdMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context, ILogger<CorrelationIdMiddleware> logger)
{
context.Items["correlation-id"] = Guid.NewGuid().ToString("n");
await next(context);
}
}
// factory-activated
public sealed class BudgetMiddleware(IBudget budget) : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (!budget.TryConsume())
{
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
return; // short-circuits: nothing further down runs
}
await next(context);
}
}
Both work, including IMiddlewareFactory activation, HttpContext.Items, RequestServices, endpoint routing with route-value binding, and Response.OnStarting callbacks.
Isolation: one instance per application
IMiddlewareSender is scoped, and publishes the scope it was resolved from as the request's RequestServices. Open one scope per simulated application and each gets its own scoped state and its own middleware instances, with no view of any other — while they all share one composed pipeline.
using var shop = provider.CreateScope();
using var warehouse = provider.CreateScope();
await shop.ServiceProvider.GetRequiredService<IMiddlewareSender>().SendAsync(request);
| Lifetime | Meaning |
|---|---|
| Singleton | shared by every instance — stateless infrastructure |
| Scoped | one per application instance — where an app's own mutable state belongs |
Keys and scopes answer different questions and compose: AddMiddlewareMediator("shop", …) distinguishes one kind of application, a scope distinguishes one instance.
What you get without asking for it
A web server does a good deal around the pipeline that no middleware ever requests, and middleware is written expecting all of it. This package does the same, so a step that works under Kestrel works here:
- An exception that escapes the pipeline becomes a 500, rather than reaching the caller. In a simulation, one application crashing must not take down the one that called it.
traceparentandbaggagecontinue the caller's trace.Activity.Currentis set for the duration of the dispatch, under the nameMicrosoft.AspNetCore.Hosting.HttpRequestInthat tracing tools already look for, so a call from one simulated instance to another shows up as one trace.- Log lines carry
RequestIdandRequestPath. With a thousand instances in one process, this is what makes a log readable. - Metrics and diagnostic events under the usual names —
http.server.request.duration,http.server.active_requests,BeginRequest,EndRequest,UnhandledException. - Responses are framed:
Content-Lengthcomputed from the body, plusDateandServer. A declared length that disagrees with the body is refused rather than sent. - Requests are identified as
instance:sequence, the shape a server uses forconnection:sequence.
Each is skipped when nothing is listening, so none of it costs anything you are not using.
Simulated time
Date and the recorded durations come from the registered TimeProvider. A simulation that advances in steps of its own registers one and its responses are stamped with its time rather than the machine's:
services.AddSingleton<TimeProvider>(mySimulationClock);
services.AddMiddlewareMediator(cfg => cfg.Configure(ConfigurePipeline));
Why it scales
Composing the chain reads every registration and activates every convention-based middleware — and that happens once, on first dispatch. Afterwards a dispatch costs one HttpContext.
| Measurement | Result |
|---|---|
| Opening 2 000 instances | 1.5 ms |
| Memory per idle instance | ~144 bytes |
| 8 000 dispatches through routing and two middlewares | 74 ms |
Relationship to Personix.Mediator
Siblings, not layers. Personix.Mediator dispatches typed requests to typed handlers; this one dispatches HTTP-shaped requests through middleware. Neither depends on the other, and this package is separate precisely so that consumers of the plain mediator — console, MAUI, Blazor WebAssembly — are not forced to carry the ASP.NET Core shared framework.
License
MIT © Personix
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net11.0 is compatible. |
-
net11.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
2.0.0 — performs the work a web host does around every request: an escaping exception becomes a 500 instead of reaching the caller, traceparent and baggage continue the caller's trace, log lines carry RequestId and RequestPath, responses are framed with Content-Length, Date and Server, and metrics and diagnostic events are published under the usual names. Timestamps and durations come from the registered TimeProvider.