2

I am creating an gRPC service and we decided to choose the code first approach with protobuf-net. Now I am running into a scenario where we have a couple of classes that need to be wrapped. We do not want to define KnownTypes in the MyMessage class (just a sample name to illustrate the problem). So I am trying to use the Any type which currently gives me some struggle with packing.

The sample code has the MyMessage which defines some header values and has to possiblity to deliver any type as payload.

[ProtoContract]
public class MyMessage 
{
  [ProtoMember(1)] public int HeaderValue1 { get; set; }
  [ProtoMember(2)] public string HeaderValue2 { get; set; }
  [ProtoMember(3)] public Google.Protobuf.WellknownTypes.Any Payload { get; set; }
}

[ProtoContract]
public class Payload1 
{
  [ProtoMember(1)] public bool Data1   { get; set; }
  [ProtoMember(2)] public string Data2 { get; set; }
}

[ProtoContract]
public class Payload2 
{
  [ProtoMember(1)] public string Data1 { get; set; }
  [ProtoMember(2)] public string Data2 { get; set; }  
}

Somewhere in the code I construct my message with a payload ...

  Payload2 payload = new Payload2 {
    Data1 = "abc",
    Data2 = "def"
  };
  
  MyMessage msg = new MyMessage 
  {
    HeaderValue1 = 123,
    HeaderValue2 = "iAmHeaderValue2",
    Payload = Google.Protobuf.WellknownTypes.Any.Pack(payload)
  };

Which doesn't work because Payload1 and Payload2 need to implement Google.Protobuf.IMessage. Since I can't figure out how and do not find a lot information how to do it at all I am wondering if I am going a wrong path.

  • How is it intedend to use Any in protobuf-net?
  • Is there a simple (yet compatible) way to pack a C# code first class into Google.Protobuf.WellknownTypes.Any?
  • Do I really need to implement Google.Protobuf.IMessage?
monty
  • 7,888
  • 16
  • 63
  • 100
  • It's probably easiest if you just convert your Payload to a `Dictionary`. `[ProtoInclude]` seems to be used for inheritance. IMessage looks like manual serialization (read / write bytes of fields). – Charles Dec 07 '21 at 13:06

2 Answers2

0

Firstly, since you say "where we have a couple of classes that need to be wrapped" (emphasis mine), I wonder if what you actually want here is oneof rather than Any. protobuf-net has support for the oneof concept, although it isn't obvious from a code-first perspective. But imagine we had (in a contract-first sense):

syntax = "proto3";
message SomeType {
    oneof Content {
       Foo foo = 1;
       Bar bar = 2;
       Blap blap = 3;
    }
}

message Foo {}
message Bar {}
message Blap {}

This would be implemented (via the protobuf-net schema tools) as:

private global::ProtoBuf.DiscriminatedUnionObject __pbn__Content;

[global::ProtoBuf.ProtoMember(1, Name = @"foo")]
public Foo Foo
{
    get => __pbn__Content.Is(1) ? ((Foo)__pbn__Content.Object) : default;
    set => __pbn__Content = new global::ProtoBuf.DiscriminatedUnionObject(1, value);
}
public bool ShouldSerializeFoo() => __pbn__Content.Is(1);
public void ResetFoo() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__Content, 1);

[global::ProtoBuf.ProtoMember(2, Name = @"bar")]
public Bar Bar
{
    get => __pbn__Content.Is(2) ? ((Bar)__pbn__Content.Object) : default;
    set => __pbn__Content = new global::ProtoBuf.DiscriminatedUnionObject(2, value);
}
public bool ShouldSerializeBar() => __pbn__Content.Is(2);
public void ResetBar() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__Content, 2);

[global::ProtoBuf.ProtoMember(3, Name = @"blap")]
public Blap Blap
{
    get => __pbn__Content.Is(3) ? ((Blap)__pbn__Content.Object) : default;
    set => __pbn__Content = new global::ProtoBuf.DiscriminatedUnionObject(3, value);
}
public bool ShouldSerializeBlap() => __pbn__Content.Is(3);
public void ResetBlap() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__Content, 3);

optionally with an enum to help:

public ContentOneofCase ContentCase => (ContentOneofCase)__pbn__Content.Discriminator;

public enum ContentOneofCase
{
    None = 0,
    Foo = 1,
    Bar = 2,
    Blap = 3,
}

This approach may be easier and preferable to Any.


On Any:

Short version: protobuf-net has not, to date, had any particular request to implement Any. It probably isn't a huge amount of work - simply: it hasn't yet happened. It looks like you're referencing both protobuf-net and the Google libs here, and using the Google implementation of Any. That's fine, but protobuf-net isn't going to use it at all - it doesn't know about the Google APIs in this context, so: implementing IMessage won't actually help you.

I'd be more than happy to look at Any with you, from the protobuf-net side. Ultimately, time/availability is always the limiting factor, so I prioritise features that are seeing demand. I think you may actually be the first person asking me about Any in protobuf-net.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Nice to hear from you. We already talked a lot about you today. We decided to choose Any over one of for two reasons. first MyMessage becomes a redistributable library not knowing the actual payloads. And second the projects using the library will have a "couple of 100+ messages" :-) creating a quite long list in oneof – monty Dec 07 '21 at 14:10
  • @monty well, `Any` doesn't exactly save you from that; even in the Google API, you'd then be facing a "couple of 100+" versions of `TryUnpack(...)`, or `Is(...)` calls; there isn't a "give me whatever it is, you figure it out" API: https://github.com/protocolbuffers/protobuf/blob/master/csharp/src/Google.Protobuf/WellKnownTypes/AnyPartial.cs (although if we *do* implement an `Any` in protobuf-net, I would hope that this is something we can improve on) – Marc Gravell Dec 07 '21 at 14:37
  • :-) Funny I am the first to ask about any. Thought that would be common use case. Since I think the clarification would bust the comments section I allowed my self to drop a mail to you directly. – monty Dec 09 '21 at 07:59
  • @MarcGravell Any update on this? Or did you find a workaround? I am in a similar situation. Been stuck for a couple of days, until I found this. Without this functionality it seems I will have to drop protobuf-net.grpc I can only use Any because i don't want a generic library to have any details of the implementations message DataGroupModel { google.protobuf.Any grid_rows = 1; TelerikGroupModel grid_group_model = 2; repeated DataGroupModel sub_groups = 3; } I don't mind having to use Any.Pack/Unpack but this is not possible without IMessage – Hairydruidy Feb 07 '22 at 10:39
  • @Hairydruidy I haven't yet had demand/need to implement full Any support, except for very terse mentions like this; can we do it? Absolutely, in theory. I'd be interested in discussing your scenario more, to understand what "success" looks like here, but "no, nothing had changed yet" – Marc Gravell Feb 08 '22 at 07:46
  • @Hairydruidy I haven't yet had demand/need to implement full Any support, except for very terse mentions like this; can we do it? Absolutely, in theory. I'd be interested in discussing your scenario more, to understand what "success" looks like here, but "no, nothing had changed yet" – Marc Gravell Feb 08 '22 at 07:46
  • @MarcGravell With regards to more info about my use case. For the frontend I am using telerik blazor components. Telerik has support for API, gRPC, ODATA, ... My backends are with gRPC (protobuf.net-grpc code first). For easy filtering telerik had a datasourcerequest/response object. While this is fully serializable with json, it is not with gRPC. To fill in the grid rows, Any is used. The above model is used in another model, which is used in another model. Therefor I really wanted to use .any otherwise I would have to create all of those models for each type of grid. – Hairydruidy Feb 08 '22 at 08:25
  • @MarcGravell In order to help other devs, I am putting all the extra logic and models needed into nuget packages. So at this point I see 2 options. Use proto and generate using the non protobuf tools, for all of my services as I don't want to have to explain 2 tools to other devs. Or create all models multiple times, which I won't do as it's too much work. – Hairydruidy Feb 08 '22 at 08:28
  • @Hairydruidy right; in that scenario, it sounds like the potential types is contextually restricted, in which case "oneof" is a much more appropriate solution than "any" (more efficient, too), and "oneof" *does* have full existing support; of interest? I can give examples, but some kind of dummy "model is roughly this" would help – Marc Gravell Feb 08 '22 at 11:48
  • @MarcGravell oneof really doesn't work for what I need. With oneof you force to main object to know about all the other objects, which is not possible. This would be a shared project, for all future projects. I do know one of is supported, as I do use it for other scenario's. For now I am giving up on this, and will drop code first (even though it pains me to do so) – Hairydruidy Feb 08 '22 at 14:20
  • @Hairydruidy fair enough, but counterpoint: the great thing about OSS is that if there's a feature that isn't yet implemented, you can always fork it, implement it, and possibly contribute it back for others to use; or help me flesh out what an appropriate implementation would look like, if the implementation side isn't your thing. It saddens me a little that OSS is so often a one way thing. – Marc Gravell Feb 09 '22 at 21:32
  • @Hairydruidy Would this work? https://stackoverflow.com/a/72408227/499112 – Tag May 27 '22 at 20:51
0

My Any implementation.

[ProtoContract(Name = "type.googleapis.com/google.protobuf.Any")]
public class Any
{
    /// <summary>Pack <paramref name="value"/></summary>
    public static Any Pack(object? value)
    {
        // Handle null
        if (value == null) return new Any { TypeUrl = null!, Value = Array.Empty<byte>() };
        // Get type
        System.Type type = value.GetType();
        // Write here
        MemoryStream ms = new MemoryStream();
        // Serialize
        RuntimeTypeModel.Default.Serialize(ms, value);
        // Create any
        Any any = new Any
        {
            TypeUrl = $"{type.Assembly.GetName().Name}/{type.FullName}",
            Value = ms.ToArray()
        };
        // Return
        return any;
    }

    /// <summary>Unpack any record</summary>
    public object? Unpack()
    {
        // Handle null
        if (TypeUrl == null || Value == null || Value.Length == 0) return null;
        // Find '/'
        int slashIx = TypeUrl.IndexOf('/');
        // Convert to C# type name
        string typename = slashIx >= 0 ? $"{TypeUrl.Substring(slashIx + 1)}, {TypeUrl.Substring(0, slashIx)}" : TypeUrl;
        // Get type (Note security issue here!)
        System.Type type = System.Type.GetType(typename, true)!;
        // Deserialize
        object value = RuntimeTypeModel.Default.Deserialize(type, Value.AsMemory());
        // Return
        return value;
    }

    /// <summary>Test type</summary>
    public bool Is(System.Type type) => $"{type.Assembly.GetName().Name}/{type.FullName}" == TypeUrl;
      
    /// <summary>Type url (using C# type names)</summary>
    [ProtoMember(1)]
    public string TypeUrl = null!;
    /// <summary>Data serialization</summary>
    [ProtoMember(2)]
    public byte[] Value = null!;

    /// <summary></summary>
    public static implicit operator Container(Any value) => new Container(value.Unpack()! );
    /// <summary></summary>
    public static implicit operator Any(Container value) => Any.Pack(value.Value);

    /// <summary></summary>
    public struct Container
    {
        /// <summary></summary>
        public object? Value;
        /// <summary></summary>
        public Container()
        {
            this.Value = null;
        }

        /// <summary></summary>
        public Container(object? value)
        {
            this.Value = value;
        }
    }
}

'System.Object' can be used as a field or property in a surrounding Container record:

[DataContract]
public class Container
{
    /// <summary></summary>
    [DataMember(Order = 1, Name = nameof(Value))]
    public Any.Container Any { get => new Any.Container(Value); set => Value = value.Value; }
    /// <summary>Object</summary>
    public object? Value;
}

Serialization

    RuntimeTypeModel.Default.Add(typeof(Any.Container), false).SetSurrogate(typeof(Any));
    var ms = new MemoryStream();
    RuntimeTypeModel.Default.Serialize(ms, new Container { Value = "Hello world" });
    Container dummy = RuntimeTypeModel.Default.Deserialize(typeof(Container), ms.ToArray().AsMemory()) as Container;
Tag
  • 286
  • 2
  • 5