26

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.

Community
  • 1
  • 1
ladenedge
  • 13,197
  • 11
  • 60
  • 117

4 Answers4

34

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));
Frederik Struck-Schøning
  • 12,981
  • 8
  • 59
  • 68
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 4
    All good, except it should be `value.ToString("o")`, not `"u"`. Otherwise you loose the offset information (time zone). – Max Shmelev Sep 20 '13 at 16:29
  • 1
    How would other protobuf libraries deserialize the output of this? If you had a class Parent with a property of type DateTimeOffset, will the other library need to have a surrogate type with a string property on it as well? or can the Parent class in the other system simply use a string type for the property (assuming in another language that doesn't have a DateTimeOffset type)? – Rush Frisby May 20 '15 at 14:46
  • 1
    I just tested the solution of @MarcGravell and can confirm what @MaxShmelev said: it must be `value.ToString("o")` to not loose the offset information. – Luca Ritossa Jan 14 '22 at 16:11
24

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));
Frederik Struck-Schøning
  • 12,981
  • 8
  • 59
  • 68
Max Shmelev
  • 3,774
  • 4
  • 26
  • 26
  • I don't quite understand how to use this in my case. I'm trying to serialize the System.Data.DataSet class. I'd really appreciate a little hint. – i know nothing Oct 27 '14 at 15:02
  • @iknownothing I don't think that is easily do-able: System.Data.DataSet is far more sophisticated than DateTimeOffset – Tamas Ionut Jan 13 '17 at 09:48
3

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>)
Frederik Struck-Schøning
  • 12,981
  • 8
  • 59
  • 68
Kit
  • 2,089
  • 1
  • 11
  • 23
0

I use this now, is compatible with the serialization as defined in google/protobuf/timestamp.proto

( In proto file: import "google/protobuf/timestamp.proto"; )

[ProtoContract]
public class DateTimeOffsetSurrogate
{
    [ProtoMember(1)]
    public Int64 Seconds { get; set; }

    [ProtoMember(2)]
    public Int32 Nanos { get; set; }

    public static implicit operator DateTimeOffsetSurrogate(DateTimeOffset value)
    {
        var totalSeconds = (value - DateTimeOffset.UnixEpoch).TotalSeconds;

        var secondsRounded = Convert.ToInt64(totalSeconds);

        return new DateTimeOffsetSurrogate { Seconds = secondsRounded, Nanos = Convert.ToInt32((totalSeconds - secondsRounded) * 1000000000) };
    }

    public static implicit operator DateTimeOffset(DateTimeOffsetSurrogate value)
    {
        return DateTimeOffset.FromUnixTimeSeconds(value.Seconds).AddTicks(value.Nanos / 100);
    }
}