I need to design and implement a service (C#, .Net 4.5+) which receives messages, converts the messages to BLL models and passes them to the BLL.
Long story:
The service will receive messages (strings containing multiple fields (no JSON, no XML just a string)) from a client.
The format of the messages can change between different protocol versions. And the service needs to support multiple different versions.
The service will need to deserialize/pars those messages into a message class (version specific).
After the parsing the service needs to convert the message class to a BLL model. The idea behind the BLL model is to unify the different protocol versions. The same message type (version independently) should be converted to the same BLL model (There will be only version of the BLL model).
The BLL model will then be passed to the BLL.
For some messages (not all) the service needs to send a response to the client. This means the service also must be able to convert a BLL model to a version specific message and send this message to the client.
I've written a demo application to show you what I've planned to do. The application "supports" two protocol version (V1 and V2). There are two messages defined MyMessage
and MyOtherMessage
. MyMessage
has changed in V2, MyOtherVersion
is in V2 still the same as in V1.
The application contains a class simulating a client which sends both messages in both versions ("totally" random). The "received" messages are getting deserialized into message classes by a version specific serializer. After the deserialization the message classes are getting converted to version independent BLL models.
The communication between the different components (receiver, serializer, converter, etc...) is done using the event aggregation pattern.
My questions:
- How would you implement the version specific message classes? (My idea was to create a namespace per version and implement the messages which has changed in this version)
- How would you select the correct serializer and converter for the version of the received message?
- How would you handle the serialization and conversion of messages which hasn't changed since the last version? (for example
MyOtherMessage
has not changed in V2, would you call the V1 serializer from the V2 serializer?) in the example I've "changed" the version of the message to V1 and published the event again, so it gets handled by the V1 serializer. - Do you think it's a good idea to work with the event aggregation pattern?
- Do you have any experience with that kind of problem (different versions)… how have you solved it?
I've uploaded the whole demo solution to Google Drive.
Messages V1:
public class MyMessage : MessageBase
{
#region Properties
public String MytSring { get; set; }
public String MySecondString { get; set; }
#endregion
}
public class MyOtherMessage : MessageBase
{
#region Properties
public Int32 MyInt { get; set; }
#endregion
}
Messages V2:
public class MyMessage : MessageBase
{
#region Properties
public String MyString { get; set; }
public Int32 MyInt { get; set; }
#endregion
}
Converter V1:
public class MessageConverter
{
#region Ctor
public MessageConverter()
{
PseudoIoC.EventAggregator.GetEvent<MessageDeserialized>()
.ObserveOn( new NewThreadScheduler() )
.Subscribe( MessageReceived );
}
#endregion
private ModelBase ConvertMyMessage( MessageDeserialized message )
{
var receivedMessage = message.Message as MyMessage;
return new MyMessageBllEquivalent
{
MytSring = receivedMessage.MytSring,
MySecondString = receivedMessage.MySecondString,
MyInt = 0
};
}
private ModelBase ConvertMyOtherMessage( MessageDeserialized message )
{
var receivedMessage = message.Message as MyOtherMessage;
return new MyOtherMessageBllEquivalent
{
MyInt = receivedMessage.MyInt
};
}
private void MessageReceived( MessageDeserialized message )
{
if ( message.Version != "1" )
return;
ModelBase result;
switch ( message.Type )
{
case MessageType.MyMessage:
result = ConvertMyMessage( message );
break;
case MessageType.MyOtherMessage:
result = ConvertMyOtherMessage( message );
break;
default:
throw new ArgumentOutOfRangeException();
}
Console.WriteLine( "Converted to {0}", result.GetType() );
//publish new event with BLL object => event gets handled by the BLL
}
}
Converter V2:
public class MessageConverter
{
#region Ctor
public MessageConverter()
{
PseudoIoC.EventAggregator.GetEvent<MessageDeserialized>()
.ObserveOn( new NewThreadScheduler() )
.Subscribe( MessageReceived );
}
#endregion
private ModelBase ConvertMyMessage( MessageDeserialized message )
{
var receivedMessage = message.Message as MyMessage;
return new MyMessageBllEquivalent
{
MytSring = receivedMessage.MyString,
MyInt = receivedMessage.MyInt,
MySecondString = String.Empty
};
}
private void MessageReceived( MessageDeserialized message )
{
if ( message.Version != "2" )
return;
ModelBase result;
switch ( message.Type )
{
case MessageType.MyMessage:
result = ConvertMyMessage( message );
break;
case MessageType.MyOtherMessage:
Console.WriteLine( "delegate V2 to V1" );
message.Version = "1";
PseudoIoC.EventAggregator.Publish( message );
return;
default:
throw new ArgumentOutOfRangeException();
}
Console.WriteLine( "Converted to {0}", result.GetType() );
//publish new event with BLL object => event gets handled by the BLL
}
}
Serializer V1:
public class MessageSerializer
{
#region Ctor
public MessageSerializer()
{
PseudoIoC.EventAggregator.GetEvent<MessageReceivedEvent>()
.ObserveOn( new NewThreadScheduler() )
.Subscribe( MessageReceived );
}
#endregion
private void MessageReceived( MessageReceivedEvent message )
{
if ( message.SchemaVersion != "1" )
return;
MessageBase result;
switch ( message.Type )
{
case MessageType.MyMessage:
result = ParsMyMessage( message );
break;
case MessageType.MyOtherMessage:
result = ParsMyOtherMessage( message );
break;
default:
throw new ArgumentOutOfRangeException();
}
PseudoIoC.EventAggregator.Publish( new MessageDeserialized
{
Type = message.Type,
Message = result,
Version = "1"
} );
}
private MessageBase ParsMyMessage( MessageReceivedEvent message )
{
var values = message.Message.Split( new[] { '{', '}' }, StringSplitOptions.RemoveEmptyEntries );
var result = new MyMessage
{
MytSring = values[0],
MySecondString = values[1]
};
Console.WriteLine( "Revived V1 MyMessage" );
return result;
}
private MessageBase ParsMyOtherMessage( MessageReceivedEvent message )
{
var values = message.Message.Split( new[] { '{', '}' }, StringSplitOptions.RemoveEmptyEntries );
var result = new MyOtherMessage
{
MyInt = values[0].ToInt32()
};
Console.WriteLine( "Revived V1 MyOtherMessage" );
return result;
}
}
Serializer V2:
public class MessageSerializer
{
#region Ctor
public MessageSerializer()
{
PseudoIoC.EventAggregator.GetEvent<MessageReceivedEvent>()
.ObserveOn( new NewThreadScheduler() )
.Subscribe( MessageReceived );
}
#endregion
private void MessageReceived( MessageReceivedEvent message )
{
if ( message.SchemaVersion != "2" )
return;
MessageBase result;
switch ( message.Type )
{
case MessageType.MyMessage:
result = ParsMyMessage( message );
break;
case MessageType.MyOtherMessage:
Console.WriteLine( "delegate V2 to V1" );
message.SchemaVersion = "1";
PseudoIoC.EventAggregator.Publish( message );
return;
default:
throw new ArgumentOutOfRangeException();
}
PseudoIoC.EventAggregator.Publish( new MessageDeserialized
{
Type = message.Type,
Message = result,
Version = "2"
} );
}
private MessageBase ParsMyMessage( MessageReceivedEvent message )
{
var values = message.Message.Split( new[] { '{', '}' }, StringSplitOptions.RemoveEmptyEntries );
var result = new MyMessage
{
MyString = values[0],
MyInt = values[1].ToInt32()
};
Console.WriteLine( "Revived V2 MyMessage" );
return result;
}
}
EventAggregator
:
public interface IEventAggregator
{
IObservable<TEvent> GetEvent<TEvent>();
void Publish<TEvent>( TEvent eventArgs );
}
public class EventAggregator : IEventAggregator
{
#region Fields
private readonly ConcurrentDictionary<Type, Object> _subjects = new ConcurrentDictionary<Type, Object>();
#endregion
public IObservable<TEvent> GetEvent<TEvent>()
{
var subject = (ISubject<TEvent>) _subjects.GetOrAdd( typeof (TEvent), x => new Subject<TEvent>() );
return subject.AsObservable();
}
public void Publish<TEvent>( TEvent eventArgs )
{
Object subject;
if ( !_subjects.TryGetValue( typeof (TEvent), out subject ) )
return;
var castedSubject = subject as ISubject<TEvent>;
castedSubject?.OnNext( eventArgs );
}
}