Send(object))For message bus, command queue, and event sourcing scenarios where the consumer deserializes a request from the wire and only has an object reference at runtime:
// Producer — serialize command + type
var command = new CreateUser("alice@example.com");
queue.Push((JsonSerializer.Serialize(command), command.GetType().AssemblyQualifiedName));
// Consumer — deserialize and dispatch without knowing TResponse
(string raw, string typeName) = queue.Pop();
var type = Type.GetType(typeName)!;
var command = JsonSerializer.Deserialize(raw, type)!;
var result = await mediator.Send(command); // runtime-typed dispatch → ValueTask<object?>
The Send(object) overload compiles to a generated type switch over every request type the compilation can see. No reflection, no MakeGenericType — the dispatch is ordinary C# the compiler checks.
switch (request) with one type-pattern case per request type it discoveredSend<TRequest, TResponse>() uses, so both routes share one cached handler and one pipeline chainobject? on the way out. Boxing happens only on this path; typed Send stays allocation-freeFrozenDictionary<Type, DispatchDelegate> populated at registration. Still no reflection, one dictionary lookup slowerEach case body lives in its own method rather than inside the switch. That is deliberate: inlined, the switch grew about 1.2 KB of machine code per request type, and past nine types it stopped fitting the JIT’s inlining budget — Send(object) went from 5.5 ns to 9.5 ns on a single added type, and kept climbing to 20 ns by eighty. Outlined, it stays flat.
Send(object) is implemented as an extension method (not an interface method). This ensures the generated typed extension methods are always preferred when the compile-time type is known:
// Typed extension wins — zero-overhead, no boxing
var result = await mediator.Send(new Ping()); // → ValueTask<int>
// Object extension — runtime dispatch, response boxed
object request = new Ping();
var result = await mediator.Send(request); // → ValueTask<object?>
Measured on .NET 11; .NET 10 is roughly twice as slow on both rows.
| Path | Cost | Boxing | Use case |
|---|---|---|---|
Send(new Ping()) |
2.7 ns | None | Normal application code |
Send((object)ping) |
6.6 ns | TResponse → object? |
Queue/bus consumers |
About 4 ns and one boxed response, against deserialization costs measured in microseconds on the paths that actually need this overload. Not a reason to avoid it where it fits.