I implemented a text based protocol that is able to process any message that follows the following format
<2 character Messsage Code>(\<2 character parameter><variable length value>)*
//in Regex format:
"(?<message>.{2})(\\(?<parameter>[^\\]{2,}))*"
In other words all messages contain 2 Letter message code and a variable number of parameters, each parameter with a 2 letter identifier and an arbitrary value.
So in it's general form the Message has the following representation
public class Message
{
public Message(string messageCode)
{
MessageCode = messageCode;
}
public string MessageCode { get; private set; }
public Dictionary<string, string> Parameters { get; set; }
public string Raw { get; internal set; }
}
The dispatcher receives said message and should deliver it to the most appropriate Processor. Each processor is able to handle one and only one Message code.
However I wanted my Processor would be able to Navigate through Parameters
in a more typed fashion, like so:
public class EchoMessage : Message
{
public EchoMessage() : base("EC") { }
[Parameter(Name = "RB")]
public string ReplyBack{ get; set; }
}
So this echo message can be represented by the string EC\RBHello World
. Obviously there are other messages that will have Properties of other types, such as int
, bool
, Enum
, or others.
My design goal was to build the necessary infracture so it would be easy to add new Messages, and their respective processors. Consider for instance the EchoProcessor
internal class EchoProcessor : MessageProcessor<EchoMessage>
{
public override ProcessResult Process(EchoMessage message)
{
return Ok(message);
}
}
TL;DR
Hopefully by now you have enough context to review the following code.
This code was completely sanitized by using tools like StyleCop
and Resharper
. (The only reason you are not going to find xml documentation is because I omitted them for brevity)
They are configured according to the conventions I follow, so there shouldn't much for you to comment on that end.
On the other hand feedback on MessageProcessor
would be much appreciated.
Interfaces
public interface IProcessor<in TMessage> : IProcessor
where TMessage : Message
{
ProcessResult Process(TMessage message);
}
public interface IProcessor
{
string Name { get; }
ProcessResult Process(Message message);
}
public interface IParser
{
Message Parse(string message);
}
public interface IDispatcher
{
ProcessResult Dispatch(string Message);
}
Implementations
[DebuggerDisplay("{" + nameof(Raw) + "}", Name = "{Message}")]
public class Message
{
public Message(string messageCode)
{
MessageCode = messageCode;
}
public string MessageCode { get; private set; }
public Dictionary<string, string> Parameters { get; set; }
public string Raw { get; internal set; }
public Message MapTo()
{
var sample = ReflectionUtils.GetSubclassesOfType<Message>()
.FirstOrDefault(m => m.Equals(this));
var properties = sample.GetType().GetTypeInfo()
.DeclaredProperties
.Select(p =>
{
var attribute = p.GetCustomAttribute<ParameterAttribute>();
var value = Parameters.ContainsKey(attribute.Name)
? Parameters[attribute.Name]
: null;
return new
{
Property = p,
Value = value
};
})
.Where(p => p.Value != null)
.ToArray();
var type = TypeAccessor.Create(sample.GetType());
foreach (var property in properties)
{
if (property.Property.PropertyType == typeof(bool))
{
type[sample, property.Property.Name] = true;
}
else
{
type[sample, property.Property.Name] =
property.Value.ConvertToType(property.Property.PropertyType);
}
}
sample.Parameters = Parameters;
sample.Raw = Raw;
return sample;
}
public void Validate() { }
// ReSharper disable NonReadonlyMemberInGetHashCode
// Those properties are assigned only in the constructor
public override int GetHashCode()
{
return MessageCode.GetHashCode();
}
public override bool Equals(object obj)
{
var other = obj as Message;
if (other == null)
{
return false;
}
return other.MessageCode == MessageCode;
}
}
public abstract class MessageProcessor : IProcessor
{
internal static readonly IProcessor Instance = new MessageProcessor<DummyMessage>();
private readonly Dictionary<Message, IProcessor> _processors =
new Dictionary<Message, IProcessor>();
private readonly Message _accept;
protected MessageProcessor(Type type)
{
if (GetType() == typeof(MessageProcessor<DummyMessage>))
{
_accept = new Message(string.Empty);
DiscoverAndRegisterProcessors();
return;
}
_accept = (Message)Activator.CreateInstance(type, true);
var msg = string.Join(" ", "Processor for message", _accept.MessageCode,
"started");
Trace.WriteLine(msg, "Info");
}
public string Name
{
get { return GetType().Name; }
}
public abstract ProcessResult Process(Message message);
internal abstract ProcessResult ProcessImpl(Message message);
protected IProcessor FindProcessor(Message message)
{
if (!_processors.ContainsKey(message))
{
return null;
}
return _processors[message];
}
private void DiscoverAndRegisterProcessors()
{
var subclasses = ReflectionUtils.GetSubclassesOfType<MessageProcessor>();
foreach (var processor in subclasses)
{
RegisterProcessor(processor);
}
}
private void RegisterProcessor(MessageProcessor processor)
{
_processors.Add(processor._accept, processor);
}
// The class is instantiated via reflection
// ReSharper disable once ClassNeverInstantiated.Local
private class DummyMessage : Message
{
public DummyMessage() : base(string.Empty) { }
}
}
public class MessageProcessor<T> : MessageProcessor, IProcessor<T>
where T : Message
{
public MessageProcessor() : base(typeof(T))
{
}
public override ProcessResult Process(Message message)
{
var processor = FindProcessor(message) as MessageProcessor;
if (processor == null)
{
var error = "Wasn't able to find a processor for the message";
return Fail(message, new NotSupportedException(error));
}
try
{
message.Validate();
Debug.Assert(processor != null, "processor != null");
return processor.ProcessImpl(message);
}
catch (Exception e)
{
return Fail(message, e);
}
}
public virtual ProcessResult Process(T message)
{
var error = "Processor was found but there is no implementation for this message.";
return Fail(message, new NotImplementedException(error));
}
internal override sealed ProcessResult ProcessImpl(Message message)
{
return Process(message.MapTo() as T);
}
protected ProcessResult Ok(Message message, object context = null)
{
return new ProcessResult()
{
Message = message,
ProcessedBy = this,
UpdatedObject = context
};
}
private ProcessResult Fail(Message message, Exception error)
{
return new ProcessResult()
{
Message = message,
ProcessedBy = this,
Error = error
};
}
}
Utilities
public static class ParseUtils
{
private static readonly Dictionary<Type, Delegate> _parsers =
new Dictionary<Type, Delegate>()
{
{ typeof(int), (Func<string, int?>)ParseInt },
{ typeof(decimal), (Func<string, decimal?>)ParseDecimal },
{ typeof(double), (Func<string, double?>)ParseDouble },
{ typeof(float), (Func<string, float?>)ParseFloat },
{ typeof(DateTime), (Func<string, DateTime?>)ParseDateTime },
{ typeof(Guid), (Func<string, Guid?>)ParseGuid }
};
public static int? ParseInt(this string value)
{
int v;
if (int.TryParse(value, out v))
{
return v;
}
return null;
}
public static float? ParseFloat(this string value)
{
float v;
if (float.TryParse(value, out v))
{
return v;
}
return null;
}
public static double? ParseDouble(this string value)
{
double v;
if (double.TryParse(value, out v))
{
return v;
}
return null;
}
public static decimal? ParseDecimal(this string value)
{
decimal v;
if (decimal.TryParse(value, out v))
{
return v;
}
return null;
}
public static DateTime? ParseDateTime(this string value)
{
DateTime v;
if (DateTime.TryParse(value, out v))
{
return v;
}
return null;
}
public static Guid? ParseGuid(this string value)
{
Guid v;
if (Guid.TryParse(value, out v))
{
return v;
}
return null;
}
public static Guid? ParseBool(this string value)
{
Guid v;
if (Guid.TryParse(value, out v))
{
return v;
}
return null;
}
public static object ConvertToType(this string value, Type type)
{
if (type == typeof(string))
{
return value;
}
if (_parsers.ContainsKey(type))
{
var converted = _parsers[type].DynamicInvoke(value);
if (converted != null)
{
return converted;
}
}
if (type.GetTypeInfo().IsEnum)
{
return Enum.ToObject(type, ParseInt(value));
}
return null;
}
}
public static class ReflectionUtils
{
public static IEnumerable<T> GetSubclassesOfType<T>(params object[] constructorArgs)
{
var types = typeof(T).GetTypeInfo().Assembly.GetTypes()
.Select(type => type.GetTypeInfo())
.Where(type => !type.IsGenericType && type.IsClass
&& !type.IsAbstract && type.IsSubclassOf(typeof(T)))
.Select(type => (T)Activator.CreateInstance(type.AsType(), constructorArgs))
.ToList();
return types;
}
}
For sake of completness a test method so you can see how you use this
private readonly IDispatcher _dispatcher = Dispatcher.Instance;
[TestMethod]
[DataRow(@"EC\RBHello World")]
public void CanDispatchEchoMessage(string sdmp)
{
var result = _dispatcher.Dispatch(sdmp);
Assert.IsNotNull(result);
Assert.IsNull(result.Error);
Assert.AreEqual(sdmp, result.Message.Raw);
var strongTypedMessage = result.Message as EchoMessage;
Assert.IsNotNull(strongTypedMessage);
Assert.AreEqual("Hello World", strongTypedMessage.ReplyBack);
Assert.AreEqual("Hello World", result.Message.Parameters["RB"]);
}
Also forgot ProcessResult
public class ProcessResult
{
public object UpdatedObject { get; set; }
public Exception Error { get; set; }
public IProcessor ProcessedBy { get; set; }
public Message Message { get; set; }
public override string ToString()
{
if (Error != null)
{
return $"Processor {ProcessedBy.Name} failed processing {Message.Raw}. {Error}";
}
return $"Processor {ProcessedBy.Name} processed {Message.Raw}";
}
}
Sorry again it seems I forgot to include the dispatcher and parser
public sealed class Parser : IParser
{
private static readonly Regex _regex =
new Regex(@"(?<message>.{2})(\\(?<parameter>[^\\]{2,}))*", RegexOptions.Compiled);
public Message Parse(string message)
{
if (!_regex.IsMatch(message))
{
throw new InvalidOperationException("Cannot parse " + message);
}
var groups = _regex.Match(message).Groups;
return new Message(groups["message"].Value)
{
Raw = message,
Parameters = groups["parameter"].Captures
.Cast<Capture>()
.ToDictionary(
c => c.Value.Substring(0, 2),
c => c.Value.Substring(2))
};
}
}
public class Dispatcher : IDispatcher
{
public static readonly IDispatcher Instance =
new Dispatcher(new Parser(), MessageProcessor.Instance);
private readonly IParser _parser;
private readonly IProcessor _processor;
private Dispatcher(IParser parser, IProcessor processor)
{
_parser = parser;
_processor = processor;
}
public ProcessResult Dispatch(string Message)
{
var message = _parser.Parse(Message);
return _processor.Process(message);
}
}