Interesting, as I'm facing the same challenges, so the doc only describe eventing, which works as expected as you can do event type composing. however in CQRS if you do at command level, like what you did, it won't work, it either continue processing your command at consumer until hit error(as the message deseralized trigger process fail) or skipped(unlikely) as your approach try to have a generic base type.
What I did are multiple experiments
goal, I have message version follow major. minor
if minor version, that means it's not breaking changes so workers(consumers) share same queue and if worker receive version not belong to itself, nack and requeue.
if major version and breaking changes then use different queue.
1st I did use a middleware, if type mismatch the PipeContext next.Send won't be invoked, so it resulting the message into skip queue. ( I want to nack and requeue, but I have no control)
2nd I tried observer, I can get receive context and comsumerMessageContext so I can get Message and check, and I thought by not calling Task.complete in the pre-consumer I can bail early, it turns out it continue.)
3rd I created a commandValidator on the Consumer and check command then call context.foward. -->>> I get what I want.
so, to me the place to do so is in the context of consuming.
Also I noticed the MT deseralize using weak type, that means if the interface properties are miss matched, it will cast properties object to nullable.
eg: interface IFoo { string name, int count, string description }, interface IBar { string name, int count}
assume ConsumerMessageContext.Message is IBar,
but outcome is ConsumerMessageContext.Message is IFoo == true.