【问题标题】:Protobuf-net object graph reference with surrogate带有代理项的 Protobuf-net 对象图参考
【发布时间】:2019-02-22 09:04:36
【问题描述】:

据我所知,从 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;
    }
}

【问题讨论】:

    标签: c# serialization protobuf-net


    【解决方案1】:

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

    我在这里的常用建议 - 这适用于任何序列化程序,而不仅仅是 protobuf-net:任何时候你发现自己遇到了序列化程序的限制,或者甚至只是在配置起来很尴尬序列化程序:停止与序列化程序作斗争。当人们尝试序列化他们的常规域模型时,几乎总是会出现这类问题,而域模型中的某些东西并不适合他们选择的序列化器。不要尝试神秘的魔法:拆分您的模型 - 让您的 域 模型非常适合您希望您的 应用程序 看到和创建的内容一个非常适合您的序列化程序的独立模型。那么你就不需要像“代理”这样的概念了。如果您使用多种序列化格式,或者在同一序列化格式中有多个不同“版本”的布局:有多个序列化模型。

    尝试在模型上为多个主机服务真的不值得。

    【讨论】:

    • 嗨@Marc,感谢您的快速回复。我需要使用代理方法来处理版本控制,我理解你的意思。如果对此有一些更新,请在此处分享评论:)
    • @ilCosmico 如果并且当我更新此内容时,它将在中远未来的时间范围内;到时候我不会记得这个问题编号了,抱歉;您必须观看发行说明
    • 嗨@Marc,看release notes 我没有看到v2 计划的功能工作,这意味着我只能在v3 中等待,对吗?
    • @ilCosmico 您还会在发行说明中看到对象跟踪是目前在 v3 中未实现的功能,我强烈倾向于“不要”不执行它”;我还没有完全排除它;但这也不是一个明显的“是的,我们绝对应该这样做”
    【解决方案2】:

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

    类

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-09-11
      • 2022-10-21
      • 1970-01-01
      • 1970-01-01
      • 2013-07-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多