Problem with versioning generic contracts

51 views
Skip to first unread message

Pavel Rodnyansky

unread,
Mar 1, 2018, 1:44:14 AM3/1/18
to masstransit-discuss
Hello. We would like to use contract versioning strategy in our project. It works well with simple types but not so well with generics. Here is the simple example.
Class IBatch<T> replesents simple collection which can contain any type. What i want is to send the concrete type IBatch<Message> to the queue and to consume interface of IBatch<IMessageV1> ( IBatch<IMessageV2>,  IBatch<IMessageV3> etc ). It doesn't work like tat apparently.
What work is: 
send IBatch<Message> and consume IBatch<Message>
or
send IBatch<IMessageV1> and consume IBatch<IMessageV1>

Both ways are not optimal of course. Any tips would be appreciated

Jack Ni

unread,
Mar 4, 2018, 2:29:17 AM3/4/18
to masstransit-discuss
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.

Jack Ni

unread,
Mar 5, 2018, 8:46:02 PM3/5/18
to masstransit-discuss
public class EntityValidationResult
    {
public IList<ValidationResult> Errors { get; private set; }
public bool IsValid => Errors.Count < 1;

public EntityValidationResult(IList<ValidationResult> errors = null)
{
Errors = errors ?? new List<ValidationResult>();
}

public override string ToString()
{
var sb = new StringBuilder();

foreach (var line in Errors)
sb.AppendLine(line.ToString());

return sb.ToString();
}
}

public static class CommandValidator
{
public static EntityValidationResult GetValidationResult<T>(object command) where T : class, new()
{
var validationResults = new List<ValidationResult>();
var commandType = command.GetType();
var commandPropes = commandType.GetProperties();
var target = new T();
var targetProps = target.GetType().GetProperties();

foreach (var commandProp in commandPropes)
{
var targetProp = targetProps.FirstOrDefault(tp => tp.Name == commandProp.Name);
if (targetProp != null)
{
var value = commandProp.GetValue(command);
targetProp.SetValue(target, Convert.ChangeType(value, targetProp.PropertyType));
}
}
var vc = new ValidationContext(target);
Validator.TryValidateObject(target, vc, validationResults, true);
return new EntityValidationResult(validationResults);
}
}

Pavel Rodnyansky

unread,
Mar 6, 2018, 1:54:06 AM3/6/18
to masstransit-discuss
Thank you for your response. Am i understanding correctly that you repack your message after validation and resent it with different type? This method indeed should work although it may lead to a noticeable perfomance impact. Unfortunately type conversion like in my example doesn't seem to be possible in c# at al, it was a misconception from my part. The answer probably is to abandon versioning completely or to do some manual reflection-magic checks like in your example.

Jack Ni

unread,
Mar 6, 2018, 2:47:12 AM3/6/18
to masstransit-discuss
The approach I did for my problem is I make the consumer like ASP.net webapi controller's modelState.

I'm not repacking. as Interface deserilzed into object already, what I did is using concreate empty object to validate the properties, don't thing it's big performance hit, as I't only loop  once,  same as Webapi. 
It works quite well, in my case, I can detect complete miss typed contract, and I have freedom of my own to deal with it

Jack Ni

unread,
Mar 6, 2018, 2:50:33 AM3/6/18
to masstransit-discuss
the code in my consumer is 


if(!CommandValidateResult.IsValid)
{
  logger.log(warning and CommandValidateResult.Errors);
  context.foward(context.DestinationAddress)

Pavel Rodnyansky

unread,
Mar 6, 2018, 5:13:08 AM3/6/18
to masstransit-discuss
I see, thanks. I will experiment with this solution, maybe it will do. 
Reply all
Reply to author
Forward
0 new messages