jefffhaynes / BinarySerializer

A declarative serialization framework for controlling formatting of data at the byte and bit level using field bindings, converters, and code.
MIT License
290 stars 62 forks source link

Serialization exception "Binding targets must not be null" when using Subtype combined with SerializeWhen attribute #225

Closed abrasat closed 1 year ago

abrasat commented 1 year ago

I have the following class definitions for my application:

    public enum ValueBlockType: UInt32
    {
        PlainValue = 1,
        ValueWithDescriptor = 2
    }

    public enum ValueDataType: UInt32
    {
        Datatype_Invalid = 0,
        Datatype_Float = 1,
        Datatype_Double = 2,
        Datatype_Int16 = 3,
        Datatype_Int32 = 4
    }

    public class ValueDescriptor
    {
        [FieldOrder(0)]
        public byte ProductId { get; set; }
        [FieldOrder(1)]
        public byte CategoryId { get; set; }
    }

    public class ValueDataHeader
    {
        [FieldOrder(0)]
        public UInt32 ParameterId { get; set; }

        [FieldOrder(1)]
        [FieldLength(8)]
        public string Name { get; set; }

        [FieldOrder(2)]
        public UInt32 NrValues { get; set; }

        [FieldOrder(3)]
        public ValueDataType DataTypeId { get; set; } = ValueDataType.Datatype_Invalid;

        [FieldOrder(4)]
        public ValueBlockType BlockType { get; set; } = ValueBlockType.PlainValue;

        [FieldOrder(5)]
        [Subtype("BlockType", ValueBlockType.PlainValue, typeof(PlainValueBlock))]
        [Subtype("BlockType", ValueBlockType.ValueWithDescriptor, typeof(ValueWithDescriptorBlock))]
        [SubtypeDefault(typeof(EmptyDataBlock))]
        public ValueBlockBody Block { get; set; }
    }

    public abstract class ValueBlockBody
    {
    }

    public class EmptyDataBlock: ValueBlockBody
    {
    }

    public abstract class PlainValueBlock: ValueBlockBody
    {
        // ???
        //[Subtype("DataTypeId", ValueDataType.Datatype_Double, typeof(DoublePlainValuesDataBody))]
        //[Subtype("DataTypeId", ValueDataType.Datatype_Float, typeof(FloatPlainValuesDataBody))]
        //[Subtype("DataTypeId", ValueDataType.Datatype_Int16, typeof(Int16PlainValuesDataBody))]
        //[Subtype("DataTypeId", ValueDataType.Datatype_Int32, typeof(Int32PlainValuesDataBody))]
        //public abstract PlainValueDataBlock DataBody { get; set; }
    }

    public abstract class PlainValueDataBlock: PlainValueBlock
    {
    }

    public abstract class ValueWithDescriptorBlock : ValueBlockBody
    {
        // ???
        //[Subtype("DataTypeId", ValueDataType.Datatype_Double, typeof(DoubleValuesWithDescriptorDataBody))]
        //[Subtype("DataTypeId", ValueDataType.Datatype_Float, typeof(FloatValuesWithDescriptorDataBody))]
        //[Subtype("DataTypeId", ValueDataType.Datatype_Int16, typeof(Int16ValuesWithDescriptorDataBody))]
        //[Subtype("DataTypeId", ValueDataType.Datatype_Int32, typeof(Int32ValuesWithDescriptorDataBody))]
        //public abstract ValueWithDescriptorDataBlock DataBody { get; set; }
    }

    public abstract class ValueWithDescriptorDataBlock: ValueWithDescriptorBlock
    {
    }

    public class DoublePlainValuesDataBody: PlainValueDataBlock
    {
        public List<double> Data { get; set; }
    }

    public class FloatPlainValuesDataBody : PlainValueDataBlock
    {
        public List<float> Data { get; set; }
    }

    public class Int16PlainValuesDataBody : PlainValueDataBlock
    {
        public List<Int16> Data { get; set; }
    }

    public class Int32PlainValuesDataBody : PlainValueDataBlock
    {
        public List<Int32> Data { get; set; }
    }

    public class ValueDescriptorDoubleValue
    {
        [FieldOrder(1)]
        public ValueDescriptor ValueDescriptor { get; set; }
        [FieldOrder(2)]
        public double Value { get; set; }
    }

    public class ValueDescriptorFloatValue
    {
        [FieldOrder(1)]
        public ValueDescriptor ValueDescriptor { get; set; }
        [FieldOrder(2)]
        public float Value { get; set; }
    }

    public class ValueDescriptorInt16Value
    {
        [FieldOrder(1)]
        public ValueDescriptor ValueDescriptor { get; set; }
        [FieldOrder(2)]
        public Int16 Value { get; set; }
    }

    public class ValueDescriptorInt32Value
    {
        [FieldOrder(1)]
        public ValueDescriptor ValueDescriptor { get; set; }
        [FieldOrder(2)]
        public Int32 Value { get; set; }
    }

    public class FloatValuesWithDescriptorDataBody : ValueWithDescriptorDataBlock
    {
        public List<ValueDescriptorFloatValue> Data { get; set; }
    }

    public class DoubleValuesWithDescriptorDataBody : ValueWithDescriptorDataBlock
    {
        public List<ValueDescriptorDoubleValue> Data { get; set; }
    }

    public class Int16ValuesWithDescriptorDataBody : ValueWithDescriptorDataBlock
    {
        public List<ValueDescriptorInt16Value> Data { get; set; }
    }

    public class Int32ValuesWithDescriptorDataBody : ValueWithDescriptorDataBlock
    {
        public List<ValueDescriptorInt32Value> Data { get; set; }
    }

How can the class definitions be modified, to be able to use the "Subtype" attribute, in view to select one of the Double, Float, Int16, Int32 derived classes, based on the DataTypeId value from the ValueDataHeader main class? Is such a binding possible? Or is there any other approach possible, to achieve the desired functionality?

abrasat commented 1 year ago

I modified the class definitions to use the "SerializeWhen" attribute. These are the actual classes:

    public enum ValueBlockType : UInt32
    {
        PlainValue = 1,
        ValueWithDescriptor = 2
    }

    public enum ValueDataType : UInt32
    {
        Datatype_Invalid = 0,
        Datatype_Float = 1,
        Datatype_Double = 2,
        Datatype_Int16 = 3,
        Datatype_Int32 = 4
    }

    public class ValueDescriptor
    {
        [FieldOrder(0)]
        public byte ProductId { get; set; }
        [FieldOrder(1)]
        public byte CategoryId { get; set; }
    }

    public class ValueDataInfo
    {
        [FieldOrder(0)]
        public ValueBlockType BlockType { get; set; } = ValueBlockType.PlainValue;

        [FieldOrder(1)]
        public UInt32 ParameterId { get; set; }

        [FieldOrder(2)]
        [FieldLength(8)]
        public string Name { get; set; }

        [FieldOrder(3)]
        public UInt32 NrValues { get; set; }

        [FieldOrder(4)]
        public ValueDataType DataTypeId { get; set; } = ValueDataType.Datatype_Invalid;

        [FieldOrder(5)]
        [SerializeWhen(nameof(BlockType), ValueBlockType.PlainValue)]
        [Subtype("DataTypeId", ValueDataType.Datatype_Double, typeof(DoublePlainValuesDataBody))]
        [Subtype("DataTypeId", ValueDataType.Datatype_Float, typeof(FloatPlainValuesDataBody))]
        [Subtype("DataTypeId", ValueDataType.Datatype_Int16, typeof(Int16PlainValuesDataBody))]
        [Subtype("DataTypeId", ValueDataType.Datatype_Int32, typeof(Int32PlainValuesDataBody))]
        [SubtypeDefault(typeof(EmptyPlainValueDataBlock))]
        public PlainValueDataBlock Block { get; set; }

        [FieldOrder(6)]
        [SerializeWhen(nameof(BlockType), ValueBlockType.ValueWithDescriptor)]
        [Subtype("DataTypeId", ValueDataType.Datatype_Double, typeof(DoubleValuesWithDescriptorDataBody))]
        [Subtype("DataTypeId", ValueDataType.Datatype_Float, typeof(FloatValuesWithDescriptorDataBody))]
        [Subtype("DataTypeId", ValueDataType.Datatype_Int16, typeof(Int16ValuesWithDescriptorDataBody))]
        [Subtype("DataTypeId", ValueDataType.Datatype_Int32, typeof(Int32ValuesWithDescriptorDataBody))]
        [SubtypeDefault(typeof(EmptyDescriptorDataBlock))]
        public ValueWithDescriptorDataBlock DescriptorBlock { get; set; }
    }

    public abstract class PlainValueDataBlock {  }
    public class EmptyPlainValueDataBlock : PlainValueDataBlock { }
    public abstract class ValueWithDescriptorDataBlock { }
    public class EmptyDescriptorDataBlock : ValueWithDescriptorDataBlock { }

    public class DoublePlainValuesDataBody : PlainValueDataBlock
    {
        public List<double> Data { get; set; }
    }

    public class FloatPlainValuesDataBody : PlainValueDataBlock
    {
        public List<float> Data { get; set; }
    }

    public class Int16PlainValuesDataBody : PlainValueDataBlock
    {
        public List<Int16> Data { get; set; }
    }

    public class Int32PlainValuesDataBody : PlainValueDataBlock
    {
        public List<Int32> Data { get; set; }
    }

    public class ValueDescriptorDoubleValue
    {
        [FieldOrder(0)]
        public ValueDescriptor ValueDescriptor { get; set; }
        [FieldOrder(1)]
        public double Value { get; set; }
    }

    public class ValueDescriptorFloatValue
    {
        [FieldOrder(0)]
        public ValueDescriptor ValueDescriptor { get; set; }
        [FieldOrder(1)]
        public float Value { get; set; }
    }

    public class ValueDescriptorInt16Value
    {
        [FieldOrder(0)]
        public ValueDescriptor ValueDescriptor { get; set; }
        [FieldOrder(1)]
        public Int16 Value { get; set; }
    }

    public class ValueDescriptorInt32Value
    {
        [FieldOrder(0)]
        public ValueDescriptor ValueDescriptor { get; set; }
        [FieldOrder(1)]
        public Int32 Value { get; set; }
    }

    public class FloatValuesWithDescriptorDataBody : ValueWithDescriptorDataBlock
    {
        public List<ValueDescriptorFloatValue> Data { get; set; }
    }

    public class DoubleValuesWithDescriptorDataBody : ValueWithDescriptorDataBlock
    {
        public List<ValueDescriptorDoubleValue> Data { get; set; }
    }

    public class Int16ValuesWithDescriptorDataBody : ValueWithDescriptorDataBlock
    {
        public List<ValueDescriptorInt16Value> Data { get; set; }
    }

    public class Int32ValuesWithDescriptorDataBody : ValueWithDescriptorDataBlock
    {
        public List<ValueDescriptorInt32Value> Data { get; set; }
    }

Then I tried to serialize some data like this:

var newData = new ValueDataInfo()
{
    BlockType = ValueBlockType.PlainValue,
    ParameterId = 1,
    DataTypeId = ValueDataType.Datatype_Int32,
    Block = new Int32PlainValuesDataBody { Data = new List<int> { 1, 2, 3 } }
};

try
{
    var binSer = new BinarySerializer();

    using (var memStream = new MemoryStream())
    {
        binSer.Serialize(memStream, newData);
        var binSerBytes = memStream.ToArray();
    }
}
catch (Exception exc)
{
}

Unfortunately this does not work as expected and throws an exception: Error serializing member 'DataTypeId'. See inner exception for detail. Inner exception: Binding targets must not be null.

The "SerializeWhen" attribute seems to be ignored and the serializer tries to Serialize all options, not only the one which is selected with the condition from "SerializeWhen". With this modification (the Subtypes are commented) the serialize works fine:

...
        [FieldOrder(6)]
        [SerializeWhen(nameof(BlockType), ValueBlockType.ValueWithDescriptor)]
        //[Subtype("DataTypeId", ValueDataType.Datatype_Double, typeof(DoubleValuesWithDescriptorDataBody))]
        //[Subtype("DataTypeId", ValueDataType.Datatype_Float, typeof(FloatValuesWithDescriptorDataBody))]
        //[Subtype("DataTypeId", ValueDataType.Datatype_Int16, typeof(Int16ValuesWithDescriptorDataBody))]
        //[Subtype("DataTypeId", ValueDataType.Datatype_Int32, typeof(Int32ValuesWithDescriptorDataBody))]
        [SubtypeDefault(typeof(EmptyDescriptorDataBlock))]
        public ValueWithDescriptorDataBlock DescriptorBlock { get; set; }
...

What is wrong with my approach?

abrasat commented 1 year ago

Could be related to this issue

briggsj commented 1 year ago

I believe your issue might be this line

 [SerializeWhen(nameof(BlockType), nameof(ValueBlockType.PlainValue))]

Should be

 [SerializeWhen(nameof(BlockType), ValueBlockType.PlainValue)]
abrasat commented 1 year ago

Thanks, but even after changing the line the same exception still occurs...

abrasat commented 1 year ago

I dont know how to fix it, but the exception occurs here in ValueNode.cs:

    private object SubtypeBindingCallback(TypeNode typeNode)
    {
        var valueType = GetValueTypeOverride();
        if (valueType == null)
        {
            throw new InvalidOperationException("Binding targets must not be null.");
        }
      ......

It should be checked if the typeNode has any SerializeWhenBindings. If yes, the SerializeWhen condition should be evaluated and if false, no further valueType checks should be performed.

abrasat commented 1 year ago

I think this code modification in SubtypeBindingCallback() should fix the problem, but cannot evaluate if any side-effects may occur:

private object SubtypeBindingCallback(TypeNode typeNode)
{
    // new code
    if (!ShouldSerialize())
    {
        return UnsetValue;
    }
    // end new code

    var valueType = GetValueTypeOverride();
    if (valueType == null)
    {
        throw new InvalidOperationException("Binding targets must not be null.");
    }
    var objectTypeNode = (ObjectTypeNode) typeNode;
...

@jefffhaynes could you please check if you can add the fix to the next release?

jefffhaynes commented 1 year ago

Unfortunately that fix causes a tiny regression. Trying to noodle through the correct behavior...

jefffhaynes commented 1 year ago

Never mind, I think the regression is just a (somewhat mind-bending) invalid use case. Looks good.