1

I did a quick search about the usage of oneof in Protobuf-net and it appears it's supported as of v2.3.0, but I can't for the life of me find any examples on exactly how one would use it!

My requirements are pretty simple, and maybe this can also be solved with [ProtoInclude] but I'm not quite sure exactly how this would work. I've got the following class:

[ProtoContract]
public class ProgressUIMessage
{
    [ProtoMember(1)]
    public int Id {get; set;}

    [ProtoMember(2)]
    public object Message {get; set;}
}

Where Message can be 1 of 8 different known types. The types do not inherit from each other at all and although the code can be changed, there's nothing all types have in common.

Using Google.Protobuf I'd expect to do something similar to this, where I have a property called Instrument that can be one of two types in the example above and then use InstrumentOneofCase to figure out which type I was given. But how do I achieve the same thing in Protobuf-net?

EDIT: I'll leave the original question as it, but perhaps a better question which more people can relate to is: how would you achieve the same thing as with this MS example in Protobuf-net? Both in terms of writing the class itself and in terms of determining what concrete type the parameter is in the end?

cogumel0
  • 2,430
  • 5
  • 29
  • 45

1 Answers1

4

The way to play with this is to take the message from the MS example you cite, and run it through protogen to see what it does - which we can do very conveniently here: https://protogen.marcgravell.com/ (note I'm adding syntax = "proto3"; at the top of the file, which is omitted in the MS example).

This gives us, among other things:

    [global::ProtoBuf.ProtoMember(2, Name = @"stock")]
    public Stock Stock
    {
        get => __pbn__instrument.Is(2) ? ((Stock)__pbn__instrument.Object) : default;
        set => __pbn__instrument = new global::ProtoBuf.DiscriminatedUnionObject(2, value);
    }
    public bool ShouldSerializeStock() => __pbn__instrument.Is(2);
    public void ResetStock() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__instrument, 2);

    private global::ProtoBuf.DiscriminatedUnionObject __pbn__instrument;

    [global::ProtoBuf.ProtoMember(3, Name = @"currency")]
    public Currency Currency
    {
        get => __pbn__instrument.Is(3) ? ((Currency)__pbn__instrument.Object) : default;
        set => __pbn__instrument = new global::ProtoBuf.DiscriminatedUnionObject(3, value);
    }
    public bool ShouldSerializeCurrency() => __pbn__instrument.Is(3);
    public void ResetCurrency() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__instrument, 3);

So we can see that it is basically using conditional serialization built on top of the DiscriminatedUnionObject type. There are actually a bunch of related types named DiscriminatedUnion* - depending on what you need to overlap, but since they're all message types here: DiscriminatedUnionObject works for us.


There's also an optional "oneof should use enum" option (under: oddly, "Options"), which if enabled, also adds:

    public InstrumentOneofCase InstrumentCase => (InstrumentOneofCase)__pbn__instrument.Discriminator;

    public enum InstrumentOneofCase
    {
        None = 0,
        Stock = 2,
        Currency = 3,
    }

Without that, you would have to use the ShouldSerialize*() methods to resolve the active case.

Hopefully that clarifies how oneof can be used with protobuf-net.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Thank you for that Marc, but could you perhaps explain how you'd write that class using the attributes from protobuf-net? I mean I imagine there's an easier way than this :) – cogumel0 Oct 20 '21 at 14:54
  • @cogumel0 ultimately, one of the aims here is to allow overlapped storage so we don't have bloated data types, and the only way to do that is via something like how `DiscriminatedUnion*` work; you *can* code it manually without that, but the code as shown above is *broadly* the intended way of implementing "oneof" with protobuf-net; I can't think of any other good ways of meeting the intended semantics; if you can: I'm all ears! – Marc Gravell Oct 20 '21 at 15:00
  • @MarcGravell If we take the full code generated by protogen then call `Serializer.GetProto` on it, is the result supposed to be the exact original .proto code again, as it appears in the MS example, or not at all? (I can ask a new question if needed). – evilmandarine Oct 20 '21 at 21:58
  • 1
    @evilmandarine it is at a minimum meant to be protocol compatible; IIRC I don't currently have any explicit way of representing `oneof` in `GetProto`/annotations; it would be nice if we did, as an addition – Marc Gravell Oct 21 '21 at 05:58