AqlaSolutions / AqlaSerializer

Binary serializer with full .NET support!
http://www.aqla.net
Other
17 stars 3 forks source link

A deferred key does not have a value yet (NoteObject call missed?) #50

Closed inethui closed 2 years ago

inethui commented 2 years ago

The following test throws "AqlaSerializer.ProtoException : A deferred key does not have a value yet (NoteObject call missed?)". Also, the method "CoreTableSurrogate.ToSurrogate" is called twice, the first call with non-null "coreTable" parameter, the second call with null "coreTable" parameter. It seems that the result of the second call is passed to "CoreTableSurrogate.FromSurrogate".

Any idea why it behaves like this? Thanks

using ProtoBuf;
using Xunit;

namespace PrototypeTests
{
    public class UnitTest
    {
        private class CoreTable
        {
            public static readonly CoreTable DefaultCoreTable = new CoreTable();
        }

        private class WithCoreTable
        {
            public CoreTable Node { get; set; }
        }

        private void SetupAqla()
        {
            var metaType = AqlaSerializer.Meta.RuntimeTypeModel.Default.Add(typeof(WithCoreTable), false);
            metaType.DefaultFormat = AqlaSerializer.ValueFormat.Reference;
            metaType.UseConstructor = false;

            var metaField = metaType.AddField(metaType.GetNextFreeFieldNumber(), "Node");
            metaField.SetSettings(x => x.V.Format = AqlaSerializer.ValueFormat.Reference);

            var metaType2 = AqlaSerializer.Meta.RuntimeTypeModel.Default[typeof(CoreTable)];
            metaType2.SetSurrogate(typeof(CoreTableSurrogate));

            var metaType3 = AqlaSerializer.Meta.RuntimeTypeModel.Default.Add(typeof(CoreTableSurrogate), false);
            metaType3.DefaultFormat = AqlaSerializer.ValueFormat.Reference;
            metaType3.UseConstructor = false;

            var metaField2 = metaType3.AddField(metaType3.GetNextFreeFieldNumber(), "IsCoreTableNull");
            metaField2.SetSettings(x => x.V.Format = AqlaSerializer.ValueFormat.Compact);

            var metaField3 = metaType3.AddField(metaType3.GetNextFreeFieldNumber(), "ObjectId");
            metaField3.SetSettings(x => x.V.Format = AqlaSerializer.ValueFormat.Compact);
        }

        [Fact]
        public void Test()
        {
            SetupAqla();

            var withCoreTable = new WithCoreTable { Node = new CoreTable() };
            var clone = AqlaSerializer.Serializer.DeepClone(withCoreTable);
            Assert.NotNull(clone.Node);
        }

        private class CoreTableSurrogate
        {
            private static int nextObjectId = 1;

            public bool IsCoreTableNull { get; set; }
            public int ObjectId { get; set; }

            [ProtoConverter]
            public static CoreTable FromSurrogate(CoreTableSurrogate surrogate)
            {
                if (surrogate == null)
                    return null;

                return surrogate.IsCoreTableNull ? null : new CoreTable();
            }

            [ProtoConverter]
            public static CoreTableSurrogate ToSurrogate(CoreTable coreTable)
            {
                var surrogate = new CoreTableSurrogate();
                surrogate.IsCoreTableNull = coreTable == null;
                surrogate.ObjectId = nextObjectId++;

                return surrogate;
            }
        }
    }
}
AqlaSolutions commented 2 years ago

Deserializer calls ToSurrogate first to convert the old value into surrogate, then does deserialization onto it and calls FromSurrogate.

AqlaSolutions commented 2 years ago

It looks like the issue happens because the code doesn't expect surrogate to change null to non-null and vice versa.

This code works without the exception:

        [AqlaSerializer.SurrogateConverter]
        public static CoreTable FromSurrogate(CoreTableSurrogate surrogate)
        {
            if (surrogate == null)
                return null;

            return new CoreTable();
        }

        [AqlaSerializer.SurrogateConverter]
        public static CoreTableSurrogate ToSurrogate(CoreTable coreTable)
        {
            if (coreTable == null)
                return null;
            var surrogate = new CoreTableSurrogate();
            surrogate.ObjectId = nextObjectId++;

            return surrogate;
        }
inethui commented 2 years ago

Thanks for the quick response. I still have a question, why is "ToSurrogate" called twice, one with not-null "coreTable" parameter, and the other with null "coreTable" parameter?

AqlaSolutions commented 2 years ago

Another (first) time it's called during serialization.

  1. ToSurrogate: Serialization
  2. ToSurrogate: Deserialization: old value (null)
  3. FromSurrogate: Deserialization: value retrieved from a stream
inethui commented 2 years ago

Thanks for the explanation. What is the purpose of 2? It's really confusing.

AqlaSolutions commented 2 years ago

You can deserialize onto an existing instance. So the existing value first must be converted into a surrogate and then deserialization is applied onto it.

inethui commented 2 years ago

Is this something internally needed by Aqla? I just can't think of a use case while I would deserialize onto an existing instance.

AqlaSolutions commented 2 years ago

It's a publicly available API, you can pass an existing object into Deserialize method.

inethui commented 2 years ago

I see. Thanks a lot for the replies. I really appreciate it.

inethui commented 2 years ago

No issue, questions are answered.