views:

218

answers:

1

I am trying to migrate existing code that uses XmlSerializer to protobuf-net due to the increased performance it offers, however I am having problems with this specific case.

I have an object[] that includes parameters that are going to be sent to a remote host (sort of a custom mini rpc facility). I know the set of types from which these parameters can be, but I cannot tell in advance in which order they are going to be sent. I have three constraints. The first is that I am running in Compact Framework, so I need something that works there. Second, as I mentioned performance is a big concern (on the serializing side) so I would rather avoid using a lot of reflection there if possible. And the most important is that I care about the order in which this parameters were sent. Using XmlSerializer it was easy just adding XmlInclude, but for fields there is nothing equivalent as far as I know in Protobuf-net. So, is there a way to do this? Here is a simplified example.

    [Serializable]
    [XmlInclude(typeof(MyType1)),
     XmlInclude(typeof(MyType2)),
     XmlInclude(typeof(MyType3))
    public class Message()
    {
         public object[] parameters;

         public Message(object[] parms)
         {
             parameters = parms; 
         }
    }

    Message m = new Message(new object[] {MyType1(), 33, "test", 
                new MyType3(), new MyType3()});
    MemoryStream ms = new MemoryStream();
    XmlSerializer xml = new XmlSerializer(typeof(Message));
    xml.Serialize(ms,xml);

That will just work with XmlSerializer, but if I try to convert it to protobuf-net I will get a "No default encoding for Object" message.

The best I came up with is to use generics and [ProtoInclude] as seen in this example. Since I can have different object types within the array this doesn't quite make it. I added a generic List for each potential type and a property with [ProtoIgnore] with type object[] to add them and get them. I have to use reflection when adding them (to know in which array to put each item) which is not desirable and I still can't preserve the ordering as I just extract all the items on each list one by one and put them into a new object[] array on the property get.

I wonder if there is a way to accomplish this?


I tried what Marc suggested below, but I couldn't get it to work. I think I may have misunderstood something.

Using the code you wrote. I thought I should use MessageParam Create to generate MessageParam objects to add to the list. So basically I added a constructor to Message like this:

public Message(object[] parms)
{
    foreach (object o in parms)
    {
        parameters.Add(MessageParam.Create(o));
    }
}

But, if i do that I will get "Unexpected type found during serialization; types must be included with ProtoIncludeAttribute; found MessageParam`1 passed as MessageParam" because I assume the serializer is expecting the non-generic version. Did I misunderstand your suggestion? If so, what is the right thing to do?

+1  A: 

object is going to be problematic. I would try something more like:

[ProtoContract]
class Message
{
    private readonly List<MessageParam> parameters = new List<MessageParam>();
    [ProtoMember(1)]
    public List<MessageParam> Parameters { get { return parameters; } }
}
[ProtoContract]
[ProtoInclude(3, typeof(MessageParam<int>))]
[ProtoInclude(4, typeof(MessageParam<float>))]
[ProtoInclude(5, typeof(MessageParam<DateTime>))]
//...known types...
abstract class MessageParam {
    public abstract object UntypedValue { get; set; }
    public static MessageParam<T> Create<T>(T value) {
        return new MessageParam<T> { Value = value };
    }
    public static MessageParam CreateDynamic(object value)
    {
        Type type = value.GetType();
        switch (Type.GetTypeCode(value.GetType()))
        {
            // special cases
            case TypeCode.Int32: return Create((int)value);
            case TypeCode.Single: return Create((float)value);
            case TypeCode.DateTime: return Create((DateTime)value);
            // fallback in case we forget to add one, or it isn't a TypeCode
            default:
                MessageParam param = (MessageParam)Activator.CreateInstance(
                    typeof(MessageParam<>).MakeGenericType(type));
                param.UntypedValue = value;
                return param;
        }
    }
}
[ProtoContract]
sealed class MessageParam<T> : MessageParam
{
    [ProtoMember(1)]
    public T Value { get; set; }
    public override object UntypedValue
    {
        get { return Value; }
        set { Value = (T)value; }
    }
}

Note that the unreleased "v2" code offers much more ability to define the relationships at runtime rather than through attributes (which is quite limiting here).

Marc Gravell
Thanks Marc! That looks pretty good. I can see where this is going, but I can't get it to work. I updated my question to show what I did. Perhaps I misunderstood something.Oh, and I would be really happy to try v2 on the CF when it comes out! This is great stuff!
cloudraven
Yes, that does the trick! Thanks a lot!
cloudraven