Search code examples
c#serializationbond

Nested objects with Bond


I'm trying to use Microsoft Bond to serialize nested objects. But Bond throws internal errors (such KeyNotFoundException).

My classes:

interface IFoo
{
}

[Bond.Schema]
class Foo1 : IFoo
{
     [Bond.Id(0)]
     public string Foo1Field { get; set; }
}

[Bond.Schema]
class Bar
{
     [Bond.Id(0)]
     public IFoo SomeFooInstance { get; set; }
}

Then I create an instance and serialize:

var src = new Bar() { SomeFooInstance = new Foo1() { Foo1Field = "Str" }};

var output = new OutputBuffer();
var writer = new CompactBinaryWriter<OutputBuffer>(output);

Serialize.To(writer, src);

var input = new InputBuffer(output.Data);
var reader = new CompactBinaryReader<InputBuffer>(input);

var dst = Deserialize<Bar>.From(reader);

But I'm getting exceptions (such KeyNotFoundException) at Serialize.To(writer, src);.

I also tried to add [Bond.Schema] to IFoo, but then the Deserialize<Bar>.From(reader); fails...

How can I serialize Bar class that contains some Foo class with Bond without getting exceptions like that?


Solution

  • If you want to use interfaces with behavioral differences (and not structural differences), the trick is to provide the deserializer with a factory so that it knows how to create a concrete instance of IFoo when it needs to. Notice that the implementations do not have the the [Bond.Schema] attribute, as they both implement the IFoo schema.

    namespace NS
    {       
        using System;
        using Bond.IO.Unsafe;
        using Bond.Protocols;
    
        internal static class Program
        {
            [Bond.Schema]
            interface IFoo
            {
                [Bond.Id(10)]
                string FooField { get; set; }
            }
    
            [Bond.Schema]
            class Bar
            {
                [Bond.Id(20)]
                public IFoo SomeFooInstance { get; set; }
            }
    
            class AlwaysUppercaseFoo : IFoo
            {
                private string fooField;
    
                public string FooField
                {
                    get
                    {
                        return fooField;
                    }
    
                    set
                    {
                        fooField = value.ToUpperInvariant();
                    }
                }
            }
    
            class IdentityFoo : IFoo
            {
                public string FooField { get; set; }
            }
    
            public static Expression NewAlwaysUppercaseFoo(Type type, Type schemaType, params Expression[] arguments)
            {
                if (schemaType == typeof(IFoo))
                {
                    return Expression.New(typeof(AlwaysUppercaseFoo));
                }
    
                // tell Bond we don't handle the requested type, so it should use it's default behavior
                return null;
            }
    
            public static Expression NewIdentityFoo(Type type, Type schemaType, params Expression[] arguments)
            {
                if (schemaType == typeof(IFoo))
                {
                    return Expression.New(typeof(IdentityFoo));
                }
    
                // tell Bond we don't handle the requested type, so it should use it's default behavior
                return null;
            }
    
            public static void Main(string[] args)
            {
                var src = new Bar() { SomeFooInstance = new IdentityFoo() { FooField = "Str" } };
    
                var output = new OutputBuffer();
                var writer = new CompactBinaryWriter<OutputBuffer>(output);
    
                Bond.Serialize.To(writer, src);
    
                {
                    var input = new InputBuffer(output.Data);
                    var deserializer = new Bond.Deserializer<CompactBinaryReader<InputBuffer>>(typeof(Bar), NewAlwaysUppercaseFoo);
                    var reader = new CompactBinaryReader<InputBuffer>(input);
                    var dst = deserializer.Deserialize<Bar>(reader);
                    Debug.Assert(dst.SomeFooInstance.FooField == "STR");
                }
    
                {
                    var input = new InputBuffer(output.Data);
                    var deserializer = new Bond.Deserializer<CompactBinaryReader<InputBuffer>>(typeof(Bar), NewIdentityFoo);
                    var reader = new CompactBinaryReader<InputBuffer>(input);
                    var dst = deserializer.Deserialize<Bar>(reader);
                    Debug.Assert(dst.SomeFooInstance.FooField == "Str");
                }
            }
        }
    }
    

    If you need both behavioral and structural differences, then you'll need to pair this with polymorphism and a bonded<IFoo> field so that you can delay deserialization until you have enough type information to select the proper implementation. (Polymorphism is explicit and opt-in in Bond.)

    I'd show an example of this, but while writing up this answer on 2018-02-21, I found a bug in the handling of classes with [Bond.Schema] that implement interfaces with [Bond.Schema]: the fields from the interface are omitted.

    For now, the workaround would be to use inheritance with classes and use virtual properties. For example:

    namespace NS
    {
        using System;
        using Bond.IO.Unsafe;
        using Bond.Protocols;
    
        internal static class Program
        {
            enum FooKind
            {
                Unknown = 0,
                AlwaysUppercase = 1,
                Identity = 2,
            }
    
            // intentionally a class to work around https://github.com/Microsoft/bond/issues/801 but simulate an interface somewhat
            [Bond.Schema]
            class IFoo
            {
                [Bond.Id(0)]
                public virtual string FooField { get; set; }
            }
    
            [Bond.Schema]
            class Bar
            {
                [Bond.Id(0)]
                public Bond.IBonded<IFoo> SomeFooInstance { get; set; }
    
                [Bond.Id(1)]
                public FooKind Kind { get; set; }
            }
    
            [Bond.Schema]
            class AlwaysUppercaseFoo : IFoo
            {
                private string fooField;
    
                public override string FooField
                {
                    get
                    {
                        return fooField;
                    }
    
                    set
                    {
                        fooField = value.ToUpperInvariant();
                    }
                }
    
                [Bond.Id(0)]
                public string JustAlwaysUppercaseFooField { get; set; }
            }
    
            [Bond.Schema]
            class IdentityFoo : IFoo
            {
                [Bond.Id(42)]
                public string JustIdentityFooField { get; set; }
            }
    
            static void RoundTripAndPrint(Bar src)
            {
                var output = new OutputBuffer();
                var writer = new CompactBinaryWriter<OutputBuffer>(output);
    
                Bond.Serialize.To(writer, src);
    
                var input = new InputBuffer(output.Data);
                var reader = new CompactBinaryReader<InputBuffer>(input);
                var dst = Bond.Deserialize<Bar>.From(reader);
    
                switch (dst.Kind)
                {
                    case FooKind.Identity:
                        {
                            var fooId = dst.SomeFooInstance.Deserialize<IdentityFoo>();
                            Console.WriteLine($"IdFoo: \"{fooId.FooField}\", \"{fooId.JustIdentityFooField}\"");
                        }
                        break;
    
                    case FooKind.AlwaysUppercase:
                        {
                            var fooUc = dst.SomeFooInstance.Deserialize<AlwaysUppercaseFoo>();
                            Console.WriteLine($"UcFoo: \"{fooUc.FooField}\", \"{fooUc.JustAlwaysUppercaseFooField}\"");
                        }
                        break;
    
                    default:
                        Console.WriteLine($"Unknown Kind: {dst.Kind}");
                        break;
                }
            }
    
            public static void Main(string[] args)
            {
                var o = new OutputBuffer();
                var w = new CompactBinaryWriter<OutputBuffer>(o);
                Bond.Serialize.To(w, new IdentityFoo() { FooField = "Str", JustIdentityFooField = "id" });
    
                var src_id = new Bar()
                {
                    SomeFooInstance = new Bond.Bonded<IdentityFoo>(new IdentityFoo() { FooField = "Str", JustIdentityFooField = "id" }),
                    Kind = FooKind.Identity
                };
    
                var src_uc = new Bar()
                {
                    SomeFooInstance = new Bond.Bonded<AlwaysUppercaseFoo>(new AlwaysUppercaseFoo() { FooField = "Str", JustAlwaysUppercaseFooField = "I LIKE TO YELL!" }),
                    Kind = FooKind.AlwaysUppercase
                };
    
                RoundTripAndPrint(src_id);
                RoundTripAndPrint(src_uc);
            }
        }
    }
    

    This prints:

    IdFoo: "Str", "id"
    UcFoo: "STR", "I LIKE TO YELL!"