1

I'm upgrading from protobuf-net 2.2.1.0 to 3.2.26, and I've noticed that callback methods decorated with [ProtoBeforeSerialization] and [ProtoAfterDeserialization] are no longer invoked during serialization and deserialization.

  • In version 2.x, these attributes worked as expected.

  • I tried explicitly assigning callbacks to the MetaType in a custom RuntimeTypeModel, but that does not work with 3.2.26, although it worked with 2.x.

  • The callback assignment code used by me and a reproducer program is listed at the end of this topic.

Question:

  • Is there a breaking change in protobuf-net v3.x that affects callback invocation?

  • How can I correctly set callbacks in version 3.2.26, especially when using custom inheritance mappings with RuntimeTypeModel?

Below is the helper class that contains the custom RuntimeTypeModel, and it usage to perform serialization and deserialization.

public static class ProtoSerializationHelper
{
    private static RuntimeTypeModel Model;

    public static byte[] Serialize<T>(T instance)
    {
        using var ms = new MemoryStream();

        CustomSerializer.Serialize(ms, instance);

        return ms.ToArray();
    }

    public static T Deserialize<T>(byte[] payload)
    {
        using var ms = new MemoryStream(payload);

        var obj = (T)CustomSerializer.Deserialize(ms, null, typeof(T));
        
        return obj;
    }

    public static RuntimeTypeModel CustomSerializer
    {
        get
        {
            if (Model == null)
            {
                Model = RuntimeTypeModel.Create();

//I have also tried using `false` as the value for `applyDefaultBehavior` flag with Model.Add API, doesn't seems have any effect.
                var iAnimal = Model.Add(typeof(IAnimal), false);

                var animal = Model.Add(typeof(Animal), true);
                var dog = Model.Add(typeof(Dog), true);


                iAnimal.AddSubType(100, typeof(Animal));
                animal.AddSubType(101, typeof(Dog));

                animal.SetCallbacks(beforeSerialize: nameof(Animal.OnSerializingCallback), afterSerialize: null, beforeDeserialize: null, afterDeserialize: nameof(Animal.OnDeserializingCallback));
                dog.SetCallbacks(beforeSerialize: nameof(Dog.OnSerializingCallback), afterSerialize: null, beforeDeserialize: null, afterDeserialize: nameof(Dog.OnDeserializingCallback));
            }

            return Model;

        }
    }
}

Below are the classes that i am trying to (de)serialize.

public interface IAnimal
{
    int Id { get; set; }
    string Name { get; set; }
    DateTime CreatedUtc { get; set; }
}

[ProtoContract]
public class Animal : IAnimal
{
    [ProtoMember(1)]
    public int Id { get; set; }

    [ProtoMember(2)]
    public string Name { get; set; } = string.Empty;

    [ProtoMember(3)]
    public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;

    internal void OnDeserializingCallback()
    {
        Console.WriteLine($"[Animal:OnDeserializing Callback] Id={Id}, Name={Name}");
    }

    internal void OnSerializingCallback()
    {
        Console.WriteLine($"[Animal:OnSerializing Callback] Id={Id}, Name={Name}");
    }
}

[ProtoContract]
public class Dog : Animal
{
    [ProtoMember(1)]
    public string Breed { get; set; } = string.Empty;

    [ProtoMember(2)]
    public int BarkVolume { get; set; }

    internal void OnDeserializingCallback()
    {
        Console.WriteLine($"[Dog:OnDeserializing Callback] Id={Id}, Name={Name}");
    }

    internal void OnSerializingCallback()
    {
        Console.WriteLine($"[Dog:OnSerializing Callback] Id={Id}, Name={Name}");
    }
}

Below is my code that performs (de)serialization.

var dog = new Dog
{
    Id = 1,
    Name = "Rex",
    Breed = "Labrador",
    BarkVolume = 7
};

var data = ProtoSerializationHelper.Serialize<IAnimal>(dog);
var clone = ProtoSerializationHelper.Deserialize<IAnimal>(data);

if (clone is Dog deserializedDog)
{
    Console.WriteLine($"Dog details -> Id={deserializedDog.Id}, Name={deserializedDog.Name}, Breed={deserializedDog.Breed}, BarkVolume={deserializedDog.BarkVolume}");
}

1 Answer 1

0

This seems to be a known bug with protobuf-net V3. See Methods with ProtoBeforeDeserialization inside derived class are not called #858:

I've noticed that using the serialization attributes like [ProtoBeforeDeserialization] inside a derived class doesn't call the method associated. Instead, if I move those methods inside the parent class "Data", they're called.

The suggested workaround is to make the methods virtual in the base class, and override them in the derived class:

mgravell commented on Jan 16, 2024
Suggestion: move the callback methods to the base class as virtual methods, and override them in the derived type...

However, your base type is an interface so you cannot use the suggested workaround as-is.

As an variation of the suggested workaround, I have found that if you add default interface methods (new in .NET 8) to your IAnimal interface, then override them in your Animal and Dog types, you can make protobuf-net call your callbacks. You have to jump through a few hoops however:

  1. Add the callback to the interface itself. It can be a dummy method or throw an exception; it won't get called as long as it is overridden in all concrete types that implement the interface. E.g.:

    public interface IAnimal
    {
        void OnDeserializingCallback() => throw new NotImplementedException();
    
  2. In every concrete type that directly implements the interface, override the interface method with a public virtual replacement. E.g.:

    [ProtoContract]
    public class Animal : IAnimal
    {
        public virtual void OnDeserializingCallback() { } // Add your code here.
    

    Making the concrete method public seems to be mandatory. If I left it as internal it was not called, instead the default interface method was called.

  3. In every derived type that inherits one of the above concrete types, override the base concrete type implementation making sure to call the base method from it. E.g.:

    [ProtoContract]
    public class Dog : Animal
    {
        public override void OnDeserializingCallback()
        {
            base.OnDeserializingCallback();
            // Add your code here.
    
  4. For the MetaType for the base interface, call SetCallbacks() and pass the names of the interface methods. E.g.:

    iAnimal.SetCallbacks(
        beforeSerialize: nameof(IAnimal.OnSerializingCallback), 
        afterSerialize: null, beforeDeserialize: null, 
        afterDeserialize: nameof(IAnimal.OnDeserializingCallback));
    

    Once this is done, there doesn't seem to be a need to set the callbacks for the MetaType for the derived types, as they aren't called.

    Also, calling SetCallbacks() in runtime seems to be mandatory. Annotating all the methods with [ProtoAfterDeserialization] and [ProtoBeforeSerialization] is not working in the current protobuf-net version, 3.2.56.

While testing, I also noticed a couple of additional problems with your code:

  1. You're not initializing your CustomSerializer in a thread-safe manner. Instead of checking directly for null you should use Lazy<T>.

  2. Protobuf-net V3 doesn't seem to round-trip the value of DateTime.Kind, so your deserialized CreatedUtc values were coming back as DateTimeKind.Unspecified.

    I'd suggest converting the value to universal in the setter, but you might also be able to set model.IncludeDateTimeKind = true.

  3. Not sure this is a bug, but your OnDeserializingCallback is being used as the afterDeserialize callback. If you meant for this to be called as soon as deserialization begins, use beforeDeserialize. If you meant it to be called last, after deserialization completes, I'd suggest renaming it to OnDeserializedCallback().

Putting that all together, your ProtoSerializationHelper should look like:

private static readonly Lazy<RuntimeTypeModel> Model = new(
    () =>
    {
        var model = RuntimeTypeModel.Create();
        model.IncludeDateTimeKind = true; // Optional, if you always want to round-trip DateTime.Kind

        var iAnimal = model.Add(typeof(IAnimal), true);
        var animal = model.Add(typeof(Animal), true);
        var dog = model.Add(typeof(Dog), true);

        iAnimal.AddSubType(100, typeof(Animal));
        animal.AddSubType(101, typeof(Dog));

        iAnimal.SetCallbacks(
            beforeSerialize: nameof(IAnimal.OnSerializingCallback), 
            afterSerialize: null, beforeDeserialize: null, 
            afterDeserialize: nameof(IAnimal.OnDeserializingCallback));
        
        //Setting the callbacks for the derived types (here Animal and Dog) doesn't seem to be necessary.

        return model;
    });

public static RuntimeTypeModel CustomSerializer => Model.Value;

And your data models should look like:

public interface IAnimal
{
    int Id { get; set; }
    string Name { get; set; }
    DateTime CreatedUtc { get; set; }

    void OnDeserializingCallback() => throw new NotImplementedException();
    void OnSerializingCallback()  => throw new NotImplementedException();
}

[ProtoContract]
public class Animal : IAnimal
{
    [ProtoMember(1)]
    public int Id { get; set; }

    [ProtoMember(2)]
    public string Name { get; set; } = string.Empty;

    DateTime createdUtc = DateTime.UtcNow;

    [ProtoMember(3)]
    public DateTime CreatedUtc { get => createdUtc; set => createdUtc = value.ToUniversalTime(); }

    public virtual void OnDeserializingCallback()
    {
        Console.WriteLine($"[Animal:OnDeserializing Callback] Id={Id}, Name={Name}");
    }

    public virtual void OnSerializingCallback()
    {
        Console.WriteLine($"[Animal:OnSerializing Callback] Id={Id}, Name={Name}");
    }
}

[ProtoContract]
public class Dog : Animal
{
    [ProtoMember(1)]
    public string Breed { get; set; } = string.Empty;

    [ProtoMember(2)]
    public int BarkVolume { get; set; }

    public override void OnDeserializingCallback()
    {
        base.OnDeserializingCallback();
        Console.WriteLine($"  [Dog:OnDeserializing Callback] Id={Id}, Name={Name}");
    }

    public override void OnSerializingCallback()
    {
        base.OnSerializingCallback();
        Console.WriteLine($"  [Dog:OnSerializing Callback] Id={Id}, Name={Name}");
    }
}

You may want to comment on Issue 858, noting the complexity of the required workaround when serializing interface types and urging that a proper fix be implemented. In some cases the "base interface" might not be modifiable making the workaround impossible. It's also not clear to me how the workaround could be used in situations where some concrete type implements multiple different interfaces, any of which might be serialized as the "base interface".

Demo fiddle here: working fiddle #1. It runs successfully with both protobuf-net 3.2.26 and the most recent version, 3.2.56. (A demo of [ProtoAfterDeserialization] and [ProtoBeforeSerialization] not working can be found here: failing fiddle #2.)

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.