带代理的 Protobuf-net 对象图参考

Protobuf-net object graph reference with surrogate

据我所知,从v2开始的protobuf-net支持引用,但它们不能与surrogate一起使用(在这种情况下抛出异常"A reference-tracked object changed reference during deserialization" )

我想知道是否有一些我没有考虑过的解决方法。

这里是重现上述异常的测试用例代码。

public class Person
{
    public Person(string name, GenderType gender)
    {
        Name = name;
        Gender = gender;
    }
    public string Name { get; set; }
    public GenderType Gender { get; set; }
}

[Flags]
public enum GenderType : byte
{
    Male = 1,
    Female = 2,
    Both = Male | Female
}

public class Family
{
    public Family(List<Person> people, Person familyHead = null)
    {
        People = people;

        FamilyHead = familyHead;
    }

    public List<Person> People { get; set; }

    public Person FamilyHead { get; set; }
}

public class PersonSurrogate
{
    public string Name { get; set; }
    public byte Gender { get; set; }

    public PersonSurrogate(string name, byte gender)
    {
        Name = name;
        Gender = gender;
    }       

    #region Static Methods

    public static implicit operator Person(PersonSurrogate surrogate)
    {
        if (surrogate == null) return null;

        return new Person(surrogate.Name, (GenderType)surrogate.Gender);

    }

    public static implicit operator PersonSurrogate(Person source)
    {
        return source == null ? null : new PersonSurrogate(source.Name, (byte)source.Gender);
    }

    #endregion       
}

public class FamilySurrogate
{
    public FamilySurrogate(List<Person> people, Person familyHead)
    {
        People = people;
        FamilyHead = familyHead;
    }

    public List<Person> People { get; set; }

    public Person FamilyHead { get; set; }

    #region Static Methods

    public static implicit operator Family(FamilySurrogate surrogate)
    {
        if (surrogate == null) return null;

        return new Family(surrogate.People, surrogate.FamilyHead);

    }

    public static implicit operator FamilySurrogate(Family source)
    {
        return source == null ? null : new FamilySurrogate(source.People, source.FamilyHead);
    }

    #endregion
}

序列化器

/// <summary>
/// Class with model for protobuf serialization
/// </summary>
public class FamilySerializer
{    
    public GenderType GenderToInclude;

    public FamilySerializer(Family family, GenderType genderToInclude = GenderType.Both)
    {
        GenderToInclude = genderToInclude;
        Family = family;

        Init();
    }

    private void Init()
    {
        Model = RuntimeTypeModel.Create();
        FillModel();
        Model.CompileInPlace();         
    }

    public FamilySerializer()
    {
        Init();
    }

    public Family Family { get; set; }
    public RuntimeTypeModel Model { get; protected set; }

    protected virtual void FillModel()
    {
        Model = RuntimeTypeModel.Create();

        Model.Add(typeof(Family), false)
            .SetSurrogate(typeof(FamilySurrogate));

        MetaType mt = Model[typeof(FamilySurrogate)];
        mt.Add(1, "People");
        mt.AddField(2, "FamilyHead").AsReference = true;  // Exception "A reference-tracked object changed reference during deserialization" - because using surrogate.
        mt.UseConstructor = false;

        Model.Add(typeof(Person), false)
            .SetSurrogate(typeof(PersonSurrogate));

        mt = Model[typeof(PersonSurrogate)]
            .Add(1, "Name")
            .Add(2, "Gender");
        mt.UseConstructor = false; // Avoids to use the parameterless constructor.
    }

    public void Save(string fileName)
    {            
        using (Stream s = File.Open(fileName, FileMode.Create, FileAccess.Write))
        {
            Model.Serialize(s, Family, new ProtoBuf.SerializationContext(){Context = this});
        }
    }

    public void Open(string fileName)
    {
        using (Stream s = File.Open(fileName, FileMode.Open, FileAccess.Read))
        {
            Family = (Family)Model.Deserialize(s, null, typeof(Family), new ProtoBuf.SerializationContext(){Context = this});
        }
    }
}

测试用例

private Family FamilyTestCase(string fileName, bool save)
{           
    if (save)
    {
        var people = new List<Person>()
        {
            new Person("Angus", GenderType.Male),
            new Person("John", GenderType.Male),
            new Person("Katrina", GenderType.Female),           
        };
        var fam = new Family(people, people[0]);

        var famSer = new FamilySerializer(fam);

        famSer.Save(fileName);

        return fam;
    }
    else
    {
        var famSer = new FamilySerializer();

        famSer.Open(fileName);

        if (Object.ReferenceEquals(fam.People[0], fam.FamilyHead))
        {
            // I'd like this condition would be satisfied
        }

        return famSer.Family;
    }
}

我认为目前这只是一个不受支持的场景,我不知道有什么方法可以让它神奇地工作;它可能是我可以在某个时候返回的东西,但是有许多更高优先级的东西会优先。

我通常的建议 - 这适用于任何序列化器,而不仅仅是 protobuf-net:任何时候你发现自己遇到序列化器的限制,甚至只是一些难以配置的东西 在序列化器中:停止与序列化器作斗争。当人们尝试序列化他们的 常规域模型 并且域模型中的某些内容不适合他们选择的序列化程序时,几乎总是会出现这种问题。不要尝试神秘的魔法:拆分你的模型 - 让你的模型非常适合你想要的应用程序 查看并创建一个非常适合您的序列化程序的 separate 模型。那么你就不需要"surrogates"这样的概念了。如果您使用多种序列化格式,或者在同一序列化格式中有多个不同的 "versions" 布局:有多个序列化模型

试图让模型服务于多个主人真的不值得头疼。

因为我知道这将不受支持,所以我找到了一种方法来处理这个问题,我想分享我的完整解决方案,以防其他人需要这个(或者如果其他人想分享更好的解决方案或改进我的方法)

public class Person
{
    public Person(string name, GenderType gender)
    {
        Name = name;
        Gender = gender;
    }
    public string Name { get; set; }
    public GenderType Gender { get; set; }
}

[Flags]
public enum GenderType : byte
{
    Male = 1,
    Female = 2,
    Both = Male | Female
}

public class Family
{
    public Family(List<Person> people, Person familyHead = null)
    {
        People = people;

        FamilyHead = familyHead;
    }

    public List<Person> People { get; set; }

    public Person FamilyHead { get; set; }
}

#region Interfaces
/// <summary>
/// Interface for objects supporting the object graph reference.
/// </summary>
public interface ISurrogateWithReferenceId
{
    /// <summary>
    /// Gets or sets the id for the object referenced more than once during the process of serialization/deserialization.
    /// </summary>
    /// <remarks>Default value is -1.</remarks>
    int ReferenceId { get; set; }
}
#endregion

public class PersonSurrogate : ISurrogateWithReferenceId
{

    /// <summary>
    /// Standard constructor.
    /// </summary>
    public PersonSurrogate(string name, byte gender)
    {
        Name = name;
        Gender = gender;
        ReferenceId = -1;
    }

    /// <summary>
    /// Private constructor for object graph reference handling.
    /// </summary>
    private PersonSurrogate(int referenceId)
    {
        ReferenceId = referenceId;
    }

    public string Name { get; set; }
    public byte Gender { get; set; }

    #region object graph reference

    /// <summary>
    /// Gets the unique id assigned to the surrogate during the process of serialization/deserialization to handle object graph reference.
    /// </summary>
    /// <remarks>Default value is -1.</remarks>
    public int ReferenceId { get; set; }

    public override bool Equals(object obj)
    {
        return base.Equals(obj) || (ReferenceId > 0 && obj is ISurrogateWithReferenceId oi && oi.ReferenceId == ReferenceId);
    }

    public override int GetHashCode()
    {
        if (ReferenceId > 0)
            return ReferenceId;

        return base.GetHashCode();
    }

    #endregion object graph reference

    protected virtual bool CheckSurrogateData(GenderType gender)
    {
        return gender == GenderType.Both || (GenderType)Gender == gender;
    }

    #region Static Methods  

    /// <summary>
    /// Converts the surrogate to the related object during the deserialization process.
    /// </summary>        
    public static implicit operator Person(PersonSurrogate surrogate)
    {
        if (surrogate == null) return null;

        if (FamilySerializer.GetCachedObject(surrogate) is Person obj)
            return obj;

        obj = new Person(surrogate.Name, (GenderType)surrogate.Gender);
        FamilySerializer.AddToCache(surrogate, obj);

        return obj;
    }

    /// <summary>
    /// Converts the object to the related surrogate during the serialization process.
    /// </summary>
    public static implicit operator PersonSurrogate(Person source)
    {
        if (source == null) return null;

        if (FamilySerializer.GetCachedObjectWithReferenceId(source) is PersonSurrogate surrogate)
        {
            surrogate = new PersonSurrogate(surrogate.ReferenceId);
        }
        else
        {
            surrogate = new PersonSurrogate(source.Name, (byte)source.Gender);
            FamilySerializer.AddToCache(source, surrogate);
        }

        return surrogate;
    }

    #endregion    
}

public class FamilySurrogate
{
    public FamilySurrogate(List<Person> people, Person familyHead)
    {
        People = people;
        FamilyHead = familyHead;
    }

    public List<Person> People { get; set; }

    public Person FamilyHead { get; set; }

    #region Static Methods

    public static implicit operator Family(FamilySurrogate surrogate)
    {
        if (surrogate == null) return null;

        return new Family(surrogate.People, surrogate.FamilyHead);

    }

    public static implicit operator FamilySurrogate(Family source)
    {
        return source == null ? null : new FamilySurrogate(source.People, source.FamilyHead);
    }

    #endregion
}

序列化器

/// <summary>
/// Class with model for protobuf serialization
/// </summary>
public class FamilySerializer
{


    public GenderType GenderToInclude;

    public FamilySerializer(Family family, GenderType genderToInclude = GenderType.Both)
    {
        GenderToInclude = genderToInclude;
        Family = family;

        Init();
    }

    private void Init()
    {
        Model = RuntimeTypeModel.Create();
        FillModel();
        Model.CompileInPlace();        
    }

    public FamilySerializer()
    {
        Init();
    }

    public Family Family { get; set; }
    public RuntimeTypeModel Model { get; protected set; }

    protected virtual void FillModel()
    {
        Model = RuntimeTypeModel.Create();

        Model.Add(typeof(Family), false)
            .SetSurrogate(typeof(FamilySurrogate));

        MetaType mt = Model[typeof(FamilySurrogate)];
        mt.Add(1, "People"); // This is a list of Person of course
        //mt.AddField(2, "FamilyHead").AsReference = true;  // Exception "A reference-tracked object changed reference during deserialization" - because using surrogate.            
        mt.Add(2, "FamilyHead");
        mt.UseConstructor = false;

        Model.Add(typeof(Person), false)
            .SetSurrogate(typeof(PersonSurrogate));

        mt = Model[typeof(PersonSurrogate)]
            .Add(1, "Name")
            .Add(2, "Gender")
            .Add(3, "ReferenceId");        
        mt.UseConstructor = false; // Avoids to use the parameter-less constructor.
    }

    #region Cache
    static FamilySerializer()
    {
        ResizeCache();
    }

    /// <summary>
    /// Resizes the cache for object graph reference handling.
    /// </summary>
    /// <param name="size"></param>
    public static void ResizeCache(int size = 500)
    {
        if (_cache != null)
        {
            foreach (var pair in _cache)
            {
                pair.Value.ResetCache();
            }
        }

        _cache = new ConcurrentDictionary<int, FamilySerializerCache>();
        for (var i = 0; i < size; i++)
            _cache.TryAdd(i, new FamilySerializerCache());
    }

    private static ConcurrentDictionary<int, FamilySerializerCache> _cache;

    /// <summary>
    /// For internal use only. Adds the specified key and value to the serializer cache for the current thread during the serialization process.
    /// </summary>
    /// <param name="objKey">The the element to add as key.</param>
    /// <param name="objValue">The value of the element to add.</param>
    /// <remarks>The <see cref="ISurrogateWithReferenceId.ReferenceId"/> is updated for <see cref="objValue"/></remarks>
    public static void AddToCache(object objKey, ISurrogateWithReferenceId objValue)
    {
        _cache[Thread.CurrentThread.ManagedThreadId].AddToCache(objKey, objValue);
    }

    /// <summary>
    /// For internal use only. Adds the specified key and value to the serializer cache for the current thread during the serialization process.
    /// </summary>
    /// <param name="objKey">The the element to add as key.</param>
    /// <param name="objValue">The value of the element to add.</param>
    /// <remarks>The <see cref="ISurrogateWithReferenceId.ReferenceId"/> is updated for <see cref="objKey"/></remarks>
    public static void AddToCache(ISurrogateWithReferenceId objKey, object objValue)
    {
        _cache[Thread.CurrentThread.ManagedThreadId].AddToCache(objKey, objValue);
    }

    /// <summary>
    /// For internal use only. Resets the cache for the current thread.
    /// </summary>
    public static void ResetCache()
    {
        _cache[Thread.CurrentThread.ManagedThreadId].ResetCache();
    }

    /// <summary>
    /// For internal use only. Gets the <see cref="ISurrogateWithReferenceId"/> associated with the specified object for the current thread.
    /// </summary>
    /// <param name="obj">The object corresponding to the value to get.</param>
    /// <returns>The related ISurrogateWithReferenceId if presents, otherwise null.</returns>
    public static ISurrogateWithReferenceId GetCachedObjectWithReferenceId(object obj)
    {
        return _cache[Thread.CurrentThread.ManagedThreadId].GetCachedObjectWithReferenceId(obj);
    }

    /// <summary>
    /// For internal use only. Gets the object associated with the specified <see cref="ISurrogateWithReferenceId"/>.
    /// </summary>
    /// <param name="surrogateWithReferenceId">The <see cref="ISurrogateWithReferenceId"/> corresponding to the object to get.</param>
    /// <returns>The related object if presents, otherwise null.</returns>
    public static object GetCachedObject(ISurrogateWithReferenceId surrogateWithReferenceId)
    {
        return _cache[Thread.CurrentThread.ManagedThreadId].GetCachedObject(surrogateWithReferenceId);
    }

    #endregion Cache

    public void Save(string fileName)
    {            
        using (Stream s = File.Open(fileName, FileMode.Create, FileAccess.Write))
        {
            Model.Serialize(s, Family, new ProtoBuf.SerializationContext(){Context = this});
        }
    }

    public void Open(string fileName)
    {
        using (Stream s = File.Open(fileName, FileMode.Open, FileAccess.Read))
        {
            Family = (Family)Model.Deserialize(s, null, typeof(Family), new ProtoBuf.SerializationContext(){Context = this});
        }
    }
}

序列化器缓存

/// <summary>
/// Helper class to support object graph reference
/// </summary>
internal class FamilySerializerCache
{
    // weak table for serialization
    // ConditionalWeakTable uses ReferenceEquals() rather than GetHashCode() and Equals() methods to do equality checks, so I can use it as a cache during the writing process to overcome the issue with objects that have overridden the GetHashCode() and Equals() methods.
    private ConditionalWeakTable<object, ISurrogateWithReferenceId> _writingTable = new ConditionalWeakTable<object, ISurrogateWithReferenceId>();

    // dictionary for deserialization
    private readonly Dictionary<ISurrogateWithReferenceId, object> _readingDictionary = new Dictionary<ISurrogateWithReferenceId, object>();

    private int _referenceIdCounter = 1;

    /// <summary>
    /// Gets the value associated with the specified key during serialization process.
    /// </summary>
    /// <param name="key">The key of the value to get.</param>
    /// <param name="value">When this method returns, contains the value associated with the specified key, if the key is found; otherwise, the default value for the type of the <paramref name="value" /> parameter. This parameter is passed uninitialized.</param>
    /// <returns>True if the internal dictionary contains an element with the specified key, otherwise False.</returns>
    private bool TryGetCachedObject(object key, out ISurrogateWithReferenceId value)
    {
        return  _writingTable.TryGetValue(key, out value);
    }

    /// <summary>
    /// Gets the value associated with the specified key during deserialization process.
    /// </summary>
    /// <param name="key">The key of the value to get.</param>
    /// <param name="value">When this method returns, contains the value associated with the specified key, if the key is found; otherwise, the default value for the type of the <paramref name="value" /> parameter. This parameter is passed uninitialized.</param>
    /// <returns>True if the internal dictionary contains an element with the specified key, otherwise False.</returns>
    private bool TryGetCachedObject(ISurrogateWithReferenceId key, out object value)
    {
        return  _readingDictionary.TryGetValue(key, out value);
    }

    /// <summary>
    /// Resets the internal dictionaries and the counter;
    /// </summary>
    public void ResetCache()
    {
        _referenceIdCounter = 1;
        _readingDictionary.Clear();

        // ConditionalWeakTable automatically removes the key/value entry as soon as no other references to a key exist outside the table, but I want to clean it as well.
        _writingTable = new ConditionalWeakTable<object, ISurrogateWithReferenceId>();
    }

    /// <summary>
    /// Adds the specified key and value to the internal dictionary during serialization process.
    /// </summary>
    /// <param name="key">The key of the element to add.</param>
    /// <param name="value">The value of the element to add.</param>
    /// <remarks>If the object implements <see cref="ISurrogateWithReferenceId"/> interface then <see cref="ISurrogateWithReferenceId.ReferenceId"/> is updated.</remarks>
    public void AddToCache(object key, ISurrogateWithReferenceId value)
    {
        if (value.ReferenceId == -1)
            value.ReferenceId = _referenceIdCounter++;

        _writingTable.Add(key, value);
    }

    /// <summary>
    /// Adds the specified key and value to the internal dictionary during deserialization process.
    /// </summary>
    /// <param name="key">The key of the element to add.</param>
    /// <param name="value">The value of the element to add.</param>
    /// <remarks>If the object implements <see cref="ISurrogateWithReferenceId"/> interface then <see cref="ISurrogateWithReferenceId.ReferenceId"/> is updated.</remarks>
    public void AddToCache(ISurrogateWithReferenceId key, object value)
    {
        if (key.ReferenceId == -1)
            key.ReferenceId = _referenceIdCounter++;

        _readingDictionary.Add(key, value);
    }

    /// <summary>
    /// Gets the <see cref="ISurrogateWithReferenceId"/> associated with the specified object.
    /// </summary>
    /// <param name="obj">The object corresponding to the value to get.</param>
    /// <returns>The related ISurrogateWithReferenceId if presents, otherwise null.</returns>
    public ISurrogateWithReferenceId GetCachedObjectWithReferenceId(object obj)
    {
        if (TryGetCachedObject(obj, out ISurrogateWithReferenceId value))
            return value;

        return null;
    }

    /// <summary>
    /// Gets the object associated with the specified <see cref="ISurrogateWithReferenceId"/>.
    /// </summary>
    /// <param name="surrogateWithReferenceId">The <see cref="ISurrogateWithReferenceId"/> corresponding to the object to get.</param>
    /// <returns>The related object if presents, otherwise null.</returns>
    public object GetCachedObject(ISurrogateWithReferenceId surrogateWithReferenceId)
    {
        if (TryGetCachedObject(surrogateWithReferenceId, out object value))
            return value;

        return null;
    }
}

测试用例

private Family FamilyTestCase(string fileName, bool save)
{           
    if (save)
    {
        var people = new List<Person>()
        {
            new Person("Angus", GenderType.Male),
            new Person("John", GenderType.Male),
            new Person("Katrina", GenderType.Female),           
        };
        var fam = new Family(people, people[0]);

        var famSer = new FamilySerializer(fam);

        famSer.Save(fileName);

        return fam;
    }
    else
    {
        var famSer = new FamilySerializer();

        famSer.Open(fileName);

        if (Object.ReferenceEquals(fam.People[0], fam.FamilyHead))
        {
            Console.WriteLine("Family head is the same than People[0]!");
        }

        return famSer.Family;
    }
}