No, it's a feature idea.
I've been working off the Mediator section in the documentation and I'm seeing this for dispatching messages that will wait for resposnes:
var client = mediator.CreateRequestClient<GetOrderStatus>();
var response = await client.GetResponse<OrderStatus>(new { OrderId = orderId });
I'm wondering if it would be nicer to simply provide message object with no type information on the response expected, and without always needing a client instance:
var response = await mediator.Send(new Order());
switch (response)
{
case OrderStatus:
// ...
break;
// ...
}
client object in betweenI don't know the internals of MassTransit, but I'm assuming the types involved on its API surface area can always be inferred through generic parameters or left as dynamic? It might even be better for decoupling to not have the caller so concerned about the type of the response until it actually has one. In the example above, having to specify <OrderStatus> seems redundant if the handler matching is done based on IConsumer<GetOrderStatus>?
Active List of Potential Enhancements
[x] Add Accept- headers to request indicating expected response types, so that newly added response types in a newer version of the consumer can be checked and the consumer can adapt/adjust accordingly to down-version clients.
if (context.IsResponseAccepted<TResponse>())
[x] Change multiple response type return type to Response<T1,T2>
if(response.Is(out Response<T1> response1))
{
}
Unfortunately, to ensure backward compatibility, an object-based deconstruct method can't be added.
Previous reply below
MassTransit is type-based, and degrading itself to an object-based pattern matching approach doesn't really support how the underlying systems operate.
For instance, consumers consume by _Type_, and the response types are used to wire up handlers within the request client so that those responses can be delivered to the request client, and ultimately the _GetResponse_ caller. It isn't possible to consume "all types" or "object", because there is no way to represent that in a message broker topology.
Mediator was built as a transport-free way to use MassTransit in the style that other mediator projects were built (such as MediatR, etc.), but using the exact same consumers, middleware, etc. that one would use in a broker-based project. Different solutions built on the same framework, think of it as a gateway to message-based systems.
I have been working on a way to improve the experience (read: trying to use C# 7, and now 9's pattern matching enhancements) for the post _GetResponse_ experience, and what I have at this point is a work in progress and not ready for merging into the develop branch. It's really only significant for multiple response types, and unfortunately at this time needs to be backwards compatible with the tuple-based approach that was first created.
What I have so far, is something like:
var response = await client.GetResponse<T1, T2>(request);
if(response.Is(out Response<T1> response1))
{
}
else if(response.Is(out Response<T2> response2))
{
}
Any timeout, cancellation, or failure by the request's consumer would be surfaced in the await client... portion as it does today. The enhancements would just make it cleaner to dispatch based upon the response type received. The current code uses a tuple of Task<Response<T1>>, Task<Response<T2>>, which requires a bunch of awaits and such that obfuscate the response handling.
Plus, it's close enough to the is style of pattern matching without the overhead of object being in the middle (if there is any, I'm not sure there is but the tasks have to be checked at some point, either up-front to get to a single object or at the point of is, which is the approach I've taken above).
Appreciating that MassTransit's implementation is made using MassTransit concepts, the issue is that front-loading knowledge of handling does challenge some of the ideas behind the mediator pattern itself.
All that to say: The degraded option is still desirable, and strictly speaking, kind of necessary for "mediator":
If I have this right, responses - by extension of clients - are more about queue addresses than they are about the subject matter of the messages themselves.
Perhaps what I actually want in MassTransit terms then is to create my own non-generic response type that is a wrapper for object. That would give me a way to DRY the creating of the client as well. But then the problem emerges that every single handler will be fired on every single message and would need to sniff that object to see if it needs to act or not.
Speaking only for the mediator implementation, it would be nice if it offered some non-generic overloads which smoothed away client details to offer this functionality. It could still offer the familiar MassTransit interfaces, but for people looking for a _classic_ :sweat_smile: mediator experience, the option would be there?
I think perhaps you may be missing the entire value of the MassTransit pipeline. The whole point is to avoid _object_, and dispatch using type information. That way, middleware, consumers, etc. are already down to the message type being consumed. The whole "is of type" is wasteful, C# is a heavily generic enabled language (with full runtime support, unlike Java which has lame generics).
The reality is, you _have_ to know the types anyway. It's a type-based language. Have you looked through how requests work in the documentation?
And I don't see how object is a classic mediator experience 鈥撀爀very other implementation uses types as well.
No, I'm pretty sure I understand and I understand reified generics pretty well having had to work with java recently :cry:. What I'm talking about here is that there's a requirement to manually provide type information that _is already available_.
It's fine to register handlers _of a type of message_, but then why do I have to specify my _type of message_ and _type of response_ when dispatching? Providing the message is enough and you can use generics to infer the type of the message by simply having it in the signature of the dispatch method:
public interface IDispatcher
{
Task<object> Dispatch<TMessage>(TMessage message);
}
// later...
{
IHandler handlers = LocateHandlers<TMessage>();
IEnumerable<object> responses = handlers.Select(handler.Handle);
}
Having signatures like this spares people from having to bake so much intimacy up front. Again, I'm not disavowing types. In fact, the limitation being imposed right now is like as if it was being done in Java. :wink:
Once you get the return of Dispatch above, it really doesn't matter what comes back, let that be up to the developers. They can always manually feed in the response type (and MassTransit can offer validation of such) on a separate overload.
Believe me, I love generics, but I don't think the current implementation is actually using them except to ask for redundant information. :pray:
note: I also understand when the generics need to be provided when the message type is an interface with an anonymous object, consider those exempt. :wink:
I'm trying to understand how having object (or dynamic, another evil imho, particularly the .NET 4 dynamic type) makes the code any different and/or better? For request/response (which is the ONLY reason to use the request client), the caller needs to know what their response options are and differentiate based upon them to proceed. Typically using the results to either complete another response, or to update a database, etc. Some work needs to be done with those responses.
I can't depict a scenario where a response would then be _dispatched_ again to more handlers. An event, sure, and that is fully supported to where a consumer could produce events that would invoke those handlers, etc. In fact, you can totally screw yourself with sagas by using send to invoke a consumer, and that consumer publishes an event, which is consumed by the same (locked) saga instance. Because it's all within the same execution context (albeit an _async_ one).
Helping me understand the why and the value, with a real example, is the next step because I still don't _see_ it. Maybe it's a mental model of how I think about communication between objects, but contracts are explicit and well defined to build well established conversations and expectations between service components.
Sorry, I corrected the dynamic, my mistake :laughing: - but I would disagree over object being outright _evil_. I'll grab your points and questions and quote them if that's okay, more to just make sure I'm not skipping anything.
By the way, thanks for going through this discussion with me. :heart:
I'll limit my points to the scope of mediator functionality for now, but this is definitely desirable: _The most generic, agnostic implementation that really only deals in IConsumer<...> types, by way of knowing the types of messages._
_the caller needs to know what their response options are and differentiate based upon them to proceed_
This conflates the _caller_ with the _mediator_. The caller can certainly still know and work with types as I'll show below, but why should the mediator care about the response type? It's just an intermediary :wink:
_I can't depict a scenario where a response would then be dispatched again to more handlers._
Not something I'm doing or hoping for here.
_...with a real example..._
I find it helpful to remember that object variables still retain their initialized types, that's part of the appeal of the recent pattern matching in C#, seen in my switch above. It doesn't strip type information, it's simply to say that you'll accept any type.
I'm also not presenting it as mutually exclusive either, it's usually straightforward to build a generic signature on top of an object implementation.
[HttpGet]
public async void Index([FromBody] ProjectIndexRequest request)
{
var mediatorRequest = mediator.CreateRequest(request);
// Currently, not possible...
// var mediatorResponse = await mediatorRequest.GetResponse</* ??? */>();
// Ideally...
object mediatorResponse = await mediatorRequest.GetResponse();
// ...you get the `mediatorResponse` that you get, its type doesn't matter (yet!).
// Now the type matters!
switch (mediatorResponse)
{
case HandledByThing1Response:
// do stuff...
break;
case HandledByThing2Response:
// do stuff...
break;
default:
throw new Exception("Wat du?");
}
}
Again, the beauty here is that I can still leverage typing for the responses, it hasn't been lost by way of being object. You can see this with HandledByThing*Response. It's just that it's all being done outside of the responsibility of MassTransit which doesn't need to involve itself in predetermining the type of the responses it receives from IConsumer<...>.
Part of the mediator pattern is that the implementation of the handlers is divorced from the caller. Enforcing a response type forces the caller to become in some way aware of the handlers.
TMessage :point_right: IHandler<TMessage> :point_right: object (or IEnumerable<object> in pubsub) :point_right: switch :point_right: No loss of strong typing
So yeah, you're still getting explicit contracts, but without having to know the specific one. As per my last comment, you can still look up handlers using something akin to the IDispatcher psuedo I shared.
_I'll mention that mediatr has something akin to this, although I am more curious about MassTransit because I planned on using this library anyway and because this implementation doesn't require marker interfaces for messages._ :+1:
And, based upon that example, I see this as equivalent:
[HttpGet]
public async void Index([FromBody] ProjectIndexRequest request)
{
using var request = mediator.CreateRequest(request);
var response = await request.GetResponse<HandledByThing1Response,HandledByThing2Response>();
if(response.Is(out Response<HandledByThing1Response> thing1))
{
}
else if(response.Is(out Response<HandledByThing2Response> thing2))
{
}
else
throw new Exception("Wat du?");
}
The underlying support in MassTransit is based upon type, so there really isn't a way to routing _any_ type to the response, only the types that MassTransit knows about will be routed to the request. It's just the way it is built internally, I don't see how it could translate otherwise.
And again, it's a good discussion that might lead to something mediator-specific, but it would break in the situation where a project was moved to using a bus/transport instead. Which leads to an inconsistent experience. That isn't to say it's impossible (it's code, nothing is impossible), it's just to say I don't see a practical way to do it with the existing message routing under the hood.
await request.GetResponse<HandledByThing1Response,HandledByThing2Response>()
This signature doesn't appeal as it forces one to provide a * number of responses. Really, what if I only care about a subset? It ends up being repetitive boilerplate. We know it's boilerpalte because response.Is(out Response<HandledByThing1Response> thing1) repeats each type in the generic parameters of GetResponse.
The underlying support in MassTransit is based upon type, so there really isn't a way to routing any type to the response, only the types that MassTransit knows about will be routed to the request. It's just the way it is built internally, I don't see how it could translate otherwise.
Again, I think I really have to emphasise this: You're not losing type information, you can still route. But the only typing information you actually are using when routing is the type of the message. Not of the response. How can it? The signature of IConsumer doesn't include the type of the response, so it's just there for the ride on the call-side, with no essential purpose in the task of dispatching and returning the result(s).
This signature doesn't appeal as it forces one to provide a * number of responses.
It does, in fact, matter. If you don't specify one of the types, and the consumer responds with that type, the client will continue to wait until the request times out or is canceled. Because the type was never routed to the client, and therefore will never satisfy the request.
The client also under the covers connects a handler for Fault<TRequest> in the event a fault is published (which doesn't happen with Mediator, since it's executed inline and actually throws the exception all the way back up to the caller).
Again, I think I really have to emphasise this: You're not losing type information, you can still route. But the only typing information you actually are using when routing is the type of the message. Not of the response. How can it? The signature of
IConsumerdoesn't include the type of the response, so it's just there for the ride on the call-side.
No, but the RespondAsync<T>(T message) call does include the type, which is used to serialize and send the message, as well as filter through any middleware, observers, etc. that may have been built into the pipeline.
What you're suggesting is possible, though it would require a new request client since the behavior is completely different from the current one (and I won't break backwards compatibility in this case). It would behave differently, for sure. And would need to be supported identically whether using mediator or a bus/transport.
The reality is, you're only suggesting late binding to the response type (and putting forth the assumption that only one message would be received and complete the request) which isn't in and of itself complicated assuming that the consumer responds directly to the sender and includes the RequestId.
If you did have something akin to:
Response response = await client.GetResponse();
You could, in theory, then do something like
return response switch
{
Response<Thing1> => blah,
Response<Thing2> => blah,
_ => throw new InvalidOperationException("well, boo.")
};
That would work pretty well - I think :laughing: - I think I was forgetting about the Response<...> type that MassTransit uses, which I have no objection towards. In fact, it's one thing I like as it gives me a context.
The syntax you have above, adapted a little and cheaply contextualized:
[HttpGet]
public async void Index([FromBody] ProjectIndexRequest request)
{
var mediatorRequest = mediator.CreateRequest(request);
var mediatorResponse = await mediatorRequest.GetResponse();
switch (mediatorResponse)
{
case Response<ProjectIndexResponse>:
break;
case Response<OtherTypeOfProjectIndexResponse>:
break;
}
}
Maybe?
I think it would work that way, you could even do the:
case Response<ProjectIndexResponse> projectIndex when projectIndex.Length > 100
I think - any of the pattern matching solutions since Response<ProjectIndex> : Response would be a thing. and since Task<Response> is a future, it would be completed with the actual received type instead of an arbitrary type.
Yeah, always! You'll always get a type, it's just that it's not codified explicitly through the mediator interfaces and the response has to be sniffed out after getting it. Let developers worry about what they get back, MassTransit's mediator APIs only need to do the ferrying around.
Just so I have it right, the Response in this case is a MassTransit.Response<...>?
This looking better as a feature idea now? :sweat_smile:
Well, it would be something like:
public interface Response :
MessageContext
{
}
public interface Response<T> :
Response
where T : class
{
}
And the new method on the request client would be:
Task<Response> GetResponse(TRequest request, CancellationToken cancellationToken = default, RequestTimeout timeout = default);
An extension method(s) could be added on IMediator as well, for a simple:
Task<Response> GetResponse<T>(this IMediator mediator, T request, CancellationToken cancellationToken = default, RequestTimeout timeout = default);
Task<Response> GetResponse<T>(this IMediator mediator, object values, CancellationToken cancellationToken = default, RequestTimeout timeout = default);
Would Response not be MassTransit.Response<...>? Or is this to facilitate a totally "pure" setup (my original suggestion, though I'm open to this also being done similar to your Response<Thing1> => blah, above).
Would
Responsenot beMassTransit.Response<...>? Or is this to facilitate a totally "pure" setup (my original suggestion).
I don't know what Response<...> means, that's a generic type and the goal of this idea is to not get the type until pattern matching.

Specifically this one.
The generics in red are just to satisfy the signature in the example.
You'd only have that once you matched it to a Response<T>, the actual object type would be some internal class that implements that interface (as well as Response, which is the future return type).
Either via:
var response = await request.GetResponse();
if(response is Response<ProjectIndex> projectIndex)
{
}
Or using the
switch(response)
{
case Response<ProjectIndex> index => ...
}
Approach.
I'm not sure I follow. Doesn't MassTransit already have its own MassTransit.Response<...> type? Why can't that be used?
I'm not sure I follow. Doesn't MassTransit already have its own
MassTransit.Response<...>type? Why can't that be used?
It does, and it is used. Refer to the interface definition above, it would just add an additional non-generic version for the return from GetResponse.
(thanks for cleaning that up :wink: )
Aaaahhhh... I see! To facilitate the signature, gotcha. To be added to this?
Yes, it would be inserted between the two so that pattern matching would work (since it only matches based upon reference as it's a single object). There is no way to deconstruct to a single result (obviously).
So, following up, is this something that can be easily done?
(Cautiously: I'm open to contributing, but I wanna make sure I don't get into a situation where someone more familiar could do it faster, properly and with less hand-holding than me.)
The core changes are actually under the hood in the type-based message router, which I doubt many beyond myself have a full understanding as to how it works. The developer surface area is minimal, based upon the method signatures discussed earlier in the thread. As I've already been looking at moving the multiple response type support from a raw tuple to a specific type (with proper deconstruct methods to avoid breaking existing code), this is something I may look at in the same undertaking.
That sounds great, I really appreciate it.

I also noticed that there's a missing object overload for mediator.CreateRequest for using anonymous objects during dispatch. Maybe that's something that can be added along the way too?
So one snag that I haven't figured out how to make work yet, is the way the type-based router dispatches messages.
The consume pipe on the mediator (and bus endpoint) is used by the request client. Before the request is sent, it connects handlers to the consume pipe, correlated by the RequestId, for each response type. The consume pipe dispatches messages to those handlers by type (ConsumeContext -> ConsumeContext<T> -> handler).
This supports the full suite of MassTransit capabilities including filters (Middleware), observers, etc. And supports things like MessageData<T> (claim check for large messages), etc. This needs to remain consistent.
The challenge with late binding the response type is that there is no ConsumeContext<T> pipe coming out of the dynamic type router. Which means the response is not handled. Which in the case of a Respond from the consumer for a type that is not consumed will throw an exception and the entire call stack falls down.
So basically, the complexity goes up significantly for a very subtle syntax concern on the request client side. And given that complexity, I'm not likely to spend a lot of immediate brain power trying to figure out how to make it work at this point.
FWIW, I wouldn't describe this as a _subtle_ syntax concern. Particularly if it's making this issue seem less valid. Forcing callers to know the type of the response ahead of time is equivalent to writing a service class from a coupling standpoint. It obviates the mediating aspect of the mediator pattern.
But yes, I'm sure there are underlying limitations in how MassTransit has worked up until now, but it sounds like a worthwhile improvement to support responses where the type won't need to be known.
I won't argue how important this is to you, I only know how important it is to me.
A contract between a service and a client is and should be explicit, which includes not only the request type, but the expected/allowed response types. It's a conversation, which includes messages in both directions.
That's a recipe for bidirectional coupling _all the time_. But more importantly, that is not the philosophy behind the mediator pattern.
Philosophy? I'm sorry, but the mediator pattern has a very simple description.
It's a behavioral pattern, related to how objects interact. It doesn't change the fact that there are well defined contracts between the client and the mediated service that ultimately handles the request.
Unless you can provide an explicit, concrete definition of why eliminating the response types from GetResponse is critical, I will continue to "not see it." Every _mediator_ framework I've seen includes the response type as part of the request definition, many don't even support multiple response types.
It's fine, I'll leave it at this point. I get that others also somehow codify the response in bidirectional calls.
The issue I see is that specifying the response type implies some knowledge about what course of action the handler will take or specifically which handler will be acting on the request. It might add up to nothing in the end though, you're right.
Well, it was a good conversation to explore the idea, and I did get a few ideas on how to better support pattern matching on the response, so that's a good outcome.
Haha, that's good! :smile:
I'll still continue using MassTransit as well FWIW, I prefer not having to put markers on my message types. And if something has to assert the response type, I'd rather it be at the call point.