Search code examples
c#eventsbond

How do I correctly use derived classes as fields of a Microsoft Bond object


So there's no confusion, when I talk through my issue I am doing so as someone who is using the compiled classes that result from Bond schemas (that is to say I use "class" instead of "struct", etc.). I feel like it makes more cognitive sense to think of it this way.

I am using Microsoft Bond and I have a main class that has several properties, one of which is an instance of a derived class.

When creating an instance of the main class I have no problem setting the property to an instance of the derived class; however when I deserialize from binary back into the main class the property is now seen as its base class.

I have tried to cast it as the derived class but that throws a runtime exception.

The examples for using derived classes in the Bond documentation/manual have you specifying the derived class at the time of deserialization, but I am not deserializing just the derived class but the main class.

Here's an example of how I have the bond schema set up

struct BaseExample
{
   0: int property1;
}

struct DerivedExample : BaseExample
{
   0: int property2;
}

struct MainExample
{
   0: BaseExample mainProperty;
}

In usage I am setting mainProperty to an instance of the DerivedExample class. What I'd expect is that after deserialization, mainProperty is still of type DerivedExample (containing property2) but what I am seeing instead is mainProperty is of type BaseExample (and doesn't contain property2)

Am I forced to use generics to do this or is there something I am missing?

EDIT: Adding examples

My code that uses the classes generated from the Bond schemas is like this.

We have a calling service that creates a message of this type and uses Bond to serialize it into a byte array before sending it on a stream.

var message = new MainExample();

var derivedExample = new DerivedExample()
{
    property1 = 1,
    property2 = 2,        
};
message.mainProperty = derivedExample;

// This block is all from the Bond examples
var output = new OutputBuffer();
var writer = new CompactBinaryWriter<OutputBuffer>(output);
Serialize.To(writer, message);

SendMessage(output.Data.Array);

Now we have a receiving service that is going to take this message off the stream and use Bond to deserialize it back into an object.

void HandleMessage(byte[] messageBA)
{
    // This block is all from the Bond examples
    var input = new InputBuffer(messageBA);
    var reader = new CompactBinaryReader<InputBuffer>(input);
    MainExample message = Deserialize<BondEvent>.From(reader);

    // mainProperty is now of type BaseExample and not DerivedExample
    message.mainProperty.property1; // is accessable
    message.mainProperty.property2; // will not compile

    DerivedExample castedProperty = message.mainProperty as DerivedExample; // fails at runtime
}

Full disclosure: I am actually using F# but I figured it would be better to do these in C#


Solution

  • The slicing behavior that you are observing when deserializing is expected with the schemas as written. The MainExample.mainProperty field is of type BaseExample, so when it is serialized, only the BaseExample fields are written. It doesn't matter which runtime type is used. Additionally, when it is deserialized, only BaseExample fields will be realized.

    When dealing with inheritance and polymorphism, Bond does not include any type information in serialized payloads: it leaves the decision about how to model this up to the schema designer. This stems from Bond's philosophy of only paying for what you use.

    Depending on the data you are modeling, I see two ways to design your schema:

    1. generics
    2. bonded

    Generics

    As mentioned in the question, the MainExample struct could be made generic:

    struct MainExample<T>
    {
        0: T mainProperty;
    }
    

    This essentially allows you to easily create a bunch of different structs with similar shapes. But these structs will not have an "is a" relationship. Methods like HandleMessage likely will also have to be generic, causing a generic cascade.

    Bonded

    To include a field in another struct that is polymorphic in type, make the field a bonded field. Bonded fields do not slice when serialized. Also, they are not immediately deserialized, so the receiving side has a chance to pick the appropriate type to deserialize into.

    In the .bond file, we'd have this:

    struct MainExample
    {
       0: bonded<BaseExample> mainProperty;
    }
    

    To serialize, the following:

    var message = new MainExample();
    
    var derivedExample = new DerivedExample()
    {
        property1 = 1,
        property2 = 2,        
    };
    message.mainProperty = new Bonded<DerivedExample>(derivedExample);
    // NB: new Bonded<BaseExample>(derivedExample) WILL slice
    

    And to deserialize:

    void HandleMessage(byte[] messageBA)
    {
        // This block is all from the Bond examples
        var input = new InputBuffer(messageBA);
        var reader = new CompactBinaryReader<InputBuffer>(input);
        MainExample message = Deserialize<BondEvent>.From(reader);
    
        DerivedExample de = message.mainProperty.Deserialize<DerivedExample>();
    }
    

    When using bonded fields for polymorphism, we will need to have some way of knowing which most-derived type to deserialize into. Sometimes this is known from context external to the payload (e.g., perhaps each of the messages handled only has one type). Other times, we need to embed this information in the common part of the payload. A common way to do this is with an enum:

    enum PropertyKind
    {
        Base;
        Derived;
    }
    
    struct MainExample
    {
        0: bonded<BaseExample> mainProperty;
        1: PropertyKind mainPropertyKind = Base;
    }
    

    There's a fully worked example of doing this kind of dispatch in the C# polymorphic_container example in the Bond repository.

    OutputBuffer.Data.Array

    I notice that in the code to send a message, there's the following line, which contains a bug:

    SendMessage(output.Data.Array);
    

    The OutputBuffer.Data property is an ArraySegment<byte>, which is used to represent a slice of some other array. This slice may be shorter than the entire array (the Count property), and it may start at an offset other than 0 (the Offset property). Most I/O libraries have an overload like SendMessage(byte[] buf, int offset, int count) that can be used in cases like this.

    The default array backing an OutputBuffer is 65K, so there's almost certainly a bunch of extra data being sent.