具有代理的Protobuf-net对象图参考

时间:2019-02-22 09:04:36

标签: c# serialization protobuf-net

据我所知,从v2开始的protobuf-net支持引用,但不能与代理结合使用(例外“反序列化期间引用跟踪的对象已更改引用” 情况)

我想知道是否有一些我没有考虑使其变通的解决方法。

下面是我的测试用例的代码,该代码再现了上面的异常。

课程

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;
    }
}

2 个答案:

答案 0 :(得分:2)

我认为目前这只是一个不受支持的情况,我不知道有一种神奇的方法可以使它神奇地工作。我可能会在某个时候回过头来谈谈,但是优先级更高的事情要优先很多。

我在这里的通常建议-这适用于任何序列化程序,而不仅仅是protobuf-net:只要您发现自己遇到了序列化程序的局限性,或者甚至是在配置中笨拙的东西,序列化程序:停止与序列化程序进行斗争。当人们尝试序列化其常规域模型时,这种问题几乎总是会出现,并且域模型中的某些内容不适合他们选择的序列化器。不用尝试神秘的魔术:分割模型-让您的模型适合您希望您的应用看到并创建的模型单独模型非常适合您的序列化程序。然后,您不需要诸如“代理人”之类的概念。如果您使用多种序列化格式,或者以相同的序列化格式具有多个不同的布局“版本”:具有多个序列化模型

尝试让模型服务于多个大师确实不值得头疼。

答案 1 :(得分:0)

由于我知道这不是受支持的方案,因此我找到了一种解决方案,我想分享我的完整解决方案,以防有人需要(或者有人希望分享更好的解决方案,或者改善我的方法)

课程

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;
    }
}