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