2
\$\begingroup\$

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);
    }
}
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Just top down a few things that I noticed... assuming you can use C# 7.

"(?<message>.{2})(\\(?<parameter>[^\\]{2,}))*"

all messages contain 2 Letter message code and a variable number of parameters, each parameter with a 2 letter identifier and an arbitrary value.

By multiple parameters I guess you mean for example: EC\RBHello\RBWorld? Are you sure this wors like that? I'm asking because as far as I know for this to work the the inner group should be repeating but it's not and the outher group is optional so EC is a valid message here. Since you didn't post code parsing this I cannot comment on that. Maybe you use some workarounds for it.


public Message MapTo()

There is something missing because reading this I automatically think MapToWhat?


public void Validate() { }

Not implemented?


// ReSharper disable NonReadonlyMemberInGetHashCode

You can get rid of such comments if you make your properties get only.


public override bool Equals(object obj)
{
    var other = obj as Message;
    if (other == null)
    {
        return false;
    }

    return other.MessageCode == MessageCode;
}

This could just be:

return obj is Message as m && Message == m.MessageCode;

if (GetType() == typeof(MessageProcessor<DummyMessage>))

There should be a helper variable for this. It's not clear what you are checking here especially that a DummyMessage is used.


protected IProcessor FindProcessor(Message message)
{
    if (!_processors.ContainsKey(message))
    {
        return null;
    }
    return _processors[message];
}

It's not necessary to read the dictionary twice. You can use TryGetValue.


public string Name
{
    get { return GetType().Name; }
}

or just

public string Name => GetType().Name;

// The class is instantiated via reflection
// ReSharper disable once ClassNeverInstantiated.Local

Reshaper has an attribute for this. Just decorate the class with the UsedImplicitlyAttribute that:

Indicates that the marked symbol is used implicitly (e.g. via reflection, in external library), so this symbol will not be marked as unused (as well as by other usage inspections).


public static int? ParseInt(this string value)

I find all these methods should be called like ParseIntOrNull so it's clear that they don't fail but return some defaul value similar to linq methods ElementAtOrDefault etc.


public static int? ParseInt(this string value)
{
    int v;
    if (int.TryParse(value, out v))
    {
        return v;
    }
    return null;
}

You can make it a one liner:

return int.TryParse(value, out var x) ? x : (int?)null;

GetSubclassesOfType

This should be called CreateSublassesOfType as this is what it does. It does not search for instances of the subclasses but it creates them.

\$\endgroup\$
1
  • \$\begingroup\$ By multiple parameters I meant multiple but different. IE EC\RBHello\XPWorld And yes. EC could be a valid message, depending on Validate implementation which is not defined yet. \$\endgroup\$ Commented Jul 16, 2017 at 13:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.