DSoftStudio Mediator

← Back to Documentation

ADR-0004: Runtime-Typed Send(object) Dispatch

Status

Released in v1.1.0

Context

In message bus / command queue architectures, the consumer deserializes a command from the wire (JSON, Protobuf, etc.) and only has the runtime Type + an object reference. The consumer needs to dispatch the command through the mediator pipeline without knowing TResponse at compile time.

// Producer
var command = new CreateUserCommand("[email protected]");
queue.Push((JsonSerializer.Serialize(command), command.GetType()));

// Consumer — only has object + Type at runtime
(string raw, Type type) = queue.Pop();
var command = JsonSerializer.Deserialize(raw, type);
await mediator.Send(command!); // <- needs runtime-typed dispatch

Current state:

The asymmetry between Publish(object) (supported) and Send(object) (not supported) blocks a common real-world pattern.

Decision

Add a runtime-typed Send(object) overload that follows the exact same architecture as Publish(object):

  1. RequestObjectDispatch — new static class with a FrozenDictionary<Type, DispatchDelegate> dispatch table, mirroring NotificationObjectDispatch.

  2. Source generatorMediatorPipelineGenerator emits RequestObjectDispatch.Register<TReq, TRes>() for every discovered request type, with a delegate that casts objectTRequest, resolves the handler/pipeline, calls Handle(), and boxes the response to object?.

  3. SenderObjectExtensions.Send(this ISender, object, CancellationToken)ValueTask<object?>extension method (not an interface method — see Design constraint: overload resolution).

  4. PrecompilePipelines() — calls RequestObjectDispatch.Freeze() after all registrations.

Design constraint: overload resolution

Send(object) is implemented as an extension method rather than an interface method. This is a deliberate design choice driven by C# overload resolution rules:

This differs from Publish(object) which is on IPublisher — there, Publish<T>(T) has one type parameter that can be inferred from the argument, so the generic overload always wins over Publish(object) without ambiguity.

Additional design constraints

Consequences

Positive

Negative

Neutral


Document History

Date Version Changes
Draft Initial ADR with design proposal
2026-03-15 v1.1.0 Released with FrozenDictionary-based dispatch