24
votes

I'm trying to serialize some objects with protobuf-net, but unfortunately they make liberal use of DateTimeOffset, which is not yet supported by protobuf-net. This leads to lots of:

No serializer defined for type: System.DateTimeOffset

Can I define my own serialization routine for unknown types? (The same question was asked earlier, but his problem was worked around.)

I'm using the latest protobuf-net beta, v2.0.0.431, under .NET 4 if it matters. I'm also using runtime definitions, so I have no way to declaratively specify how certain properties are to be handled.

3

3 Answers

31
votes

There are two ways of approaching the issue of unknown "common" types; the first is to use a shim property, for example a property that represents the value as something similar (a string or long for example):

[ProtoMember(8)]
public string Foo {
    get { ... read from the other member ... }
    set { ... assign the other member ... }
}

The other approach is a surrogate, which is a second protobuf contract that is automatically substituted. The requirements to use a surrogate are:

  • there must be a defined conversion operator (implicit or explict) between the two types (for example, DateTimeOffset and DateTimeOffsetSurrogate)
  • you then use SetSurrogate(surrogateType) to educate protobuf-net, for example RuntimeTypeModel.Default.Add(typeof(DateTimeOffset), false).SetSurrogate(typeof(DateTimeOffsetSurrogate));

the shim property is simpler, but requires repeat per-member. The surrogate is applied automatically to all instances of the type within the model. The surrogate then follows standard protobuf-net rules, so you would indicate which members to serialize, etc.

EDIT: Adding code example

using System;
using ProtoBuf;

[ProtoContract]
public class DateTimeOffsetSurrogate
{
    [ProtoMember(1)]
    public string DateTimeString { get; set; }

    public static implicit operator DateTimeOffsetSurrogate(DateTimeOffset value)
    {
        return new DateTimeOffsetSurrogate {DateTimeString = value.ToString("u")};
    }

    public static implicit operator DateTimeOffset(DateTimeOffsetSurrogate value)
    {
        return DateTimeOffset.Parse(value.DateTimeString);
    }
}

Then register it like this

RuntimeTypeModel.Default.Add(typeof(DateTimeOffset), false).SetSurrogate(typeof(DateTimeOffsetSurrogate));
22
votes

With all respect to Marc Gravell's answer, if you care about the size of serialized data, you should use the following surrogate class. The output size is 21 bytes instead of 35 bytes.

using System;
using ProtoBuf;

[ProtoContract]
public class DateTimeOffsetSurrogate
{
    [ProtoMember(1)]
    public long DateTimeTicks { get; set; }
    [ProtoMember(2)]
    public short OffsetMinutes { get; set; }

    public static implicit operator DateTimeOffsetSurrogate(DateTimeOffset value)
    {
        return new DateTimeOffsetSurrogate
        {
            DateTimeTicks = value.Ticks,
            OffsetMinutes = (short)value.Offset.TotalMinutes
        };
    }

    public static implicit operator DateTimeOffset(DateTimeOffsetSurrogate value)
    {
        return new DateTimeOffset(value.DateTimeTicks, TimeSpan.FromMinutes(value.OffsetMinutes));
    }
}

And then registering it absolutely the same way:

RuntimeTypeModel.Default.Add(typeof(DateTimeOffset), false).SetSurrogate(typeof(DateTimeOffsetSurrogate));
3
votes

Just in case any F# developers come across this question, here's an answer in F#:

[<ProtoContract>]
type DateTimeOffsetSurrogate() =
    [<ProtoMember(1)>]
    member val DateTimeString = "" with get, set
    static member public op_Implicit(value : DateTimeOffset) : DateTimeOffsetSurrogate =
        DateTimeOffsetSurrogate(DateTimeString = value.ToString("o"))
    static member public op_Implicit(value : DateTimeOffsetSurrogate) : DateTimeOffset =
        DateTimeOffset.Parse(value.DateTimeString)

It's the op_Implicit aspect which is non-obvious.

You could also adapt this to use Max's technique of using ticks to save space.

Edit: Here's how to add the surrogate to the run-time-type model thingy:

let init() =
    ProtoBuf.Meta.RuntimeTypeModel.Default.Add(typedefof<DateTimeOffset>, false).SetSurrogate(typedefof<DateTimeOffsetSurrogate>)