我有一个这样的课:
public class ComplexClass { public ConcurrentBag<SimpleClass> _simpleClassObjects; }当我序列化这个类时,它的工作原理。 但是当我尝试反序列化时
public static ComplexClass LoadComplexClass() { ComplexClass persistedComplexClass; using (var stream = new StreamReader(File.Open(jsonFilePath, FileMode.Open))) { persistedComplexClass = (ComplexClass) JsonSerializer.Create().Deserialize(stream, typeof(ComplexClass)); } return persistedComplexClass; }它抛出了异常:
Newtonsoft.Json.dll中发生了未处理的“System.InvalidCastException”类型异常
附加信息:无法将类型为'System.Collections.Concurrent.ConcurrentBag`1 [LabML.Model.Point]'的对象强制转换为'System.Collections.Generic.ICollection`1 [LabML.Model.Point]'。
此异常的根本原因是ConcurrentBag<T>不实现通用ICollection<T> ,只实现非通用ICollection 。
如何使用Json.Net解决这个问题? (我已经搜索了一段时间,但只有我发现的是将ICollection<T>映射到不在复杂类中的ConcurrentCollection 。
I have a class like this:
public class ComplexClass { public ConcurrentBag<SimpleClass> _simpleClassObjects; }When i serialize this class, it works. But when i try to deserialize
public static ComplexClass LoadComplexClass() { ComplexClass persistedComplexClass; using (var stream = new StreamReader(File.Open(jsonFilePath, FileMode.Open))) { persistedComplexClass = (ComplexClass) JsonSerializer.Create().Deserialize(stream, typeof(ComplexClass)); } return persistedComplexClass; }it throws the exception:
An unhandled exception of type 'System.InvalidCastException' occurred in Newtonsoft.Json.dll
Additional information: Unable to cast object of type 'System.Collections.Concurrent.ConcurrentBag`1[LabML.Model.Point]' to type 'System.Collections.Generic.ICollection`1[LabML.Model.Point]'.
This root cause of this exception is that the ConcurrentBag<T> doesn't implements generic ICollection<T>, only non-generic ICollection.
How to resolve this using Json.Net? (I've searched a while for this, but only what i found is about mapping an ICollection<T> to ConcurrentCollection not in Complex Classes.
最满意答案
更新
从版本10.0.3开始,Json.NET声称正确序列化ConcurrentBag<T> 。 根据发布说明 :
修复 - 修复了序列化ConcurrentStack / Queue / Bag的问题原始答案
正如你猜测的那样,问题是ConcurrentBag<T>实现ICollection和IEnumerable<T>而不是ICollection<T>所以Json.NET不知道如何向它添加项目并将其视为只读集合。 虽然ConcurrentBag<T>确实有一个参数化构造函数来获取输入集合 ,但Json.NET不会使用该构造函数,因为它在内部也具有[OnSerializing]和[OnDeserialized]回调。 当存在这些回调时,Json.NET将不使用参数化构造函数,而是抛出异常
Cannot call OnSerializing on an array or readonly list, or list created from a non-default constructor: System.Collections.Concurrent.ConcurrentBag`1[]因此,有必要为ConcurrentBag<T>创建自定义JsonConverter :
public class ConcurrentBagConverter : ConcurrentBagConverterBase { public override bool CanConvert(Type objectType) { return objectType.GetConcurrentBagItemType() != null; } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; try { var itemType = objectType.GetConcurrentBagItemType(); var method = GetType().GetMethod("ReadJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); var genericMethod = method.MakeGenericMethod(new[] { objectType, itemType }); return genericMethod.Invoke(this, new object[] { reader, objectType, itemType, existingValue, serializer }); } catch (TargetInvocationException ex) { // Wrap the TargetInvocationException in a JsonSerializationException throw new JsonSerializationException("Failed to deserialize " + objectType, ex); } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var objectType = value.GetType(); try { var itemType = objectType.GetConcurrentBagItemType(); var method = GetType().GetMethod("WriteJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); var genericMethod = method.MakeGenericMethod(new[] { objectType, itemType }); genericMethod.Invoke(this, new object[] { writer, value, serializer }); } catch (TargetInvocationException ex) { // Wrap the TargetInvocationException in a JsonSerializationException throw new JsonSerializationException("Failed to serialize " + objectType, ex); } } } public class ConcurrentBagConverter<TItem> : ConcurrentBagConverterBase { public override bool CanConvert(Type objectType) { return typeof(ConcurrentBagConverter<TItem>).IsAssignableFrom(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { return ReadJsonGeneric<ConcurrentBag<TItem>, TItem>(reader, objectType, typeof(TItem), existingValue, serializer); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { WriteJsonGeneric<ConcurrentBag<TItem>, TItem>(writer, value, serializer); } } // https://stackoverflow.com/questions/42836648/json-net-deserialize-complex-object-with-concurrent-collection-in-composition public abstract class ConcurrentBagConverterBase : JsonConverter { protected TConcurrentBag ReadJsonGeneric<TConcurrentBag, TItem>(JsonReader reader, Type collectionType, Type itemType, object existingValue, JsonSerializer serializer) where TConcurrentBag : ConcurrentBag<TItem> { if (reader.TokenType == JsonToken.Null) return null; if (reader.TokenType != JsonToken.StartArray) throw new JsonSerializationException(string.Format("Expected {0}, encountered {1} at path {2}", JsonToken.StartArray, reader.TokenType, reader.Path)); var collection = existingValue as TConcurrentBag ?? (TConcurrentBag)serializer.ContractResolver.ResolveContract(collectionType).DefaultCreator(); while (reader.Read()) { switch (reader.TokenType) { case JsonToken.Comment: break; case JsonToken.EndArray: return collection; default: collection.Add((TItem)serializer.Deserialize(reader, itemType)); break; } } // Should not come here. throw new JsonSerializationException("Unclosed array at path: " + reader.Path); } protected void WriteJsonGeneric<TConcurrentBag, TItem>(JsonWriter writer, object value, JsonSerializer serializer) where TConcurrentBag : ConcurrentBag<TItem> { // Snapshot the bag as an array and serialize the array. var array = ((TConcurrentBag)value).ToArray(); serializer.Serialize(writer, array); } } internal static class TypeExtensions { public static Type GetConcurrentBagItemType(this Type objectType) { while (objectType != null) { if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(ConcurrentBag<>)) { return objectType.GetGenericArguments()[0]; } objectType = objectType.BaseType; } return null; } } public class ConcurrentBagContractResolver : DefaultContractResolver { protected override JsonArrayContract CreateArrayContract(Type objectType) { var contract = base.CreateArrayContract(objectType); var concurrentItemType = objectType.GetConcurrentBagItemType(); if (concurrentItemType != null) { if (contract.Converter == null) contract.Converter = (JsonConverter)Activator.CreateInstance(typeof(ConcurrentBagConverter<>).MakeGenericType(new[] { concurrentItemType })); } return contract; } }然后,将通用版本应用于您的特定字段,如下所示:
public class ComplexClass { [JsonConverter(typeof(ConcurrentBagConverter<SimpleClass>))] public ConcurrentBag<SimpleClass> _simpleClassObjects; }或者,使用以下设置为任何T所有ConcurrentBag<T>全局应用通用版本:
var settings = new JsonSerializerSettings { Converters = { new ConcurrentBagConverter() }, };或者,可以使用自定义合约解析器,其性能可能比使用通用转换器稍好一些:
var settings = new JsonSerializerSettings { ContractResolver = new ConcurrentBagContractResolver(), };示例小提琴 。
话虽如此,上述仅在ConcurrentBag<T>属性或字段是读/写时才有效。 如果该成员是只读的,那么我发现即使存在转换器 ,Json.NET 9.0.1也会跳过反序列化,因为它会推断出集合成员和内容都是只读的。 (这可能是JsonSerializerInternalReader.CalculatePropertyDetails()的错误。)
作为一种变通方法,您可以使该属性可以私有设置,并使用[JsonProperty]标记它:
public class ComplexClass { ConcurrentBag<SimpleClass> m_simpleClassObjects = new ConcurrentBag<SimpleClass>(); [JsonConverter(typeof(ConcurrentBagConverter<SimpleClass>))] [JsonProperty] public ConcurrentBag<SimpleClass> _simpleClassObjects { get { return m_simpleClassObjects; } private set { m_simpleClassObjects = value; } } }或者使用代理数组属性,从而无需任何类型的转换器:
public class ComplexClass { readonly ConcurrentBag<SimpleClass> m_simpleClassObjects = new ConcurrentBag<SimpleClass>(); [JsonIgnore] public ConcurrentBag<SimpleClass> _simpleClassObjects { get { return m_simpleClassObjects; } } [JsonProperty("_simpleClassObjects")] SimpleClass[] _simpleClassObjectsArray { get { return _simpleClassObjects.ToArray(); } set { if (value == null) return; foreach (var item in value) _simpleClassObjects.Add(item); } } }Update
As of Release 10.0.3, Json.NET claims to correctly serialize ConcurrentBag<T>. According to the release notes:
Fix - Fixed serializing ConcurrentStack/Queue/BagOriginal Answer
As you surmise, the problem is that ConcurrentBag<T> implements ICollection and IEnumerable<T> but not ICollection<T> so Json.NET does not know how to add items to it and treats it as a read-only collection. While ConcurrentBag<T> does have a parameterized constructor taking an input collection, Json.NET will not use that constructor because it also, internally, has [OnSerializing] and [OnDeserialized] callbacks. Json.NET will not use a parameterized constructor when these callbacks are present, instead throwing an exception
Cannot call OnSerializing on an array or readonly list, or list created from a non-default constructor: System.Collections.Concurrent.ConcurrentBag`1[]Thus it is necessary to create a custom JsonConverter for ConcurrentBag<T>:
public class ConcurrentBagConverter : ConcurrentBagConverterBase { public override bool CanConvert(Type objectType) { return objectType.GetConcurrentBagItemType() != null; } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; try { var itemType = objectType.GetConcurrentBagItemType(); var method = GetType().GetMethod("ReadJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); var genericMethod = method.MakeGenericMethod(new[] { objectType, itemType }); return genericMethod.Invoke(this, new object[] { reader, objectType, itemType, existingValue, serializer }); } catch (TargetInvocationException ex) { // Wrap the TargetInvocationException in a JsonSerializationException throw new JsonSerializationException("Failed to deserialize " + objectType, ex); } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var objectType = value.GetType(); try { var itemType = objectType.GetConcurrentBagItemType(); var method = GetType().GetMethod("WriteJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); var genericMethod = method.MakeGenericMethod(new[] { objectType, itemType }); genericMethod.Invoke(this, new object[] { writer, value, serializer }); } catch (TargetInvocationException ex) { // Wrap the TargetInvocationException in a JsonSerializationException throw new JsonSerializationException("Failed to serialize " + objectType, ex); } } } public class ConcurrentBagConverter<TItem> : ConcurrentBagConverterBase { public override bool CanConvert(Type objectType) { return typeof(ConcurrentBagConverter<TItem>).IsAssignableFrom(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { return ReadJsonGeneric<ConcurrentBag<TItem>, TItem>(reader, objectType, typeof(TItem), existingValue, serializer); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { WriteJsonGeneric<ConcurrentBag<TItem>, TItem>(writer, value, serializer); } } // https://stackoverflow.com/questions/42836648/json-net-deserialize-complex-object-with-concurrent-collection-in-composition public abstract class ConcurrentBagConverterBase : JsonConverter { protected TConcurrentBag ReadJsonGeneric<TConcurrentBag, TItem>(JsonReader reader, Type collectionType, Type itemType, object existingValue, JsonSerializer serializer) where TConcurrentBag : ConcurrentBag<TItem> { if (reader.TokenType == JsonToken.Null) return null; if (reader.TokenType != JsonToken.StartArray) throw new JsonSerializationException(string.Format("Expected {0}, encountered {1} at path {2}", JsonToken.StartArray, reader.TokenType, reader.Path)); var collection = existingValue as TConcurrentBag ?? (TConcurrentBag)serializer.ContractResolver.ResolveContract(collectionType).DefaultCreator(); while (reader.Read()) { switch (reader.TokenType) { case JsonToken.Comment: break; case JsonToken.EndArray: return collection; default: collection.Add((TItem)serializer.Deserialize(reader, itemType)); break; } } // Should not come here. throw new JsonSerializationException("Unclosed array at path: " + reader.Path); } protected void WriteJsonGeneric<TConcurrentBag, TItem>(JsonWriter writer, object value, JsonSerializer serializer) where TConcurrentBag : ConcurrentBag<TItem> { // Snapshot the bag as an array and serialize the array. var array = ((TConcurrentBag)value).ToArray(); serializer.Serialize(writer, array); } } internal static class TypeExtensions { public static Type GetConcurrentBagItemType(this Type objectType) { while (objectType != null) { if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(ConcurrentBag<>)) { return objectType.GetGenericArguments()[0]; } objectType = objectType.BaseType; } return null; } } public class ConcurrentBagContractResolver : DefaultContractResolver { protected override JsonArrayContract CreateArrayContract(Type objectType) { var contract = base.CreateArrayContract(objectType); var concurrentItemType = objectType.GetConcurrentBagItemType(); if (concurrentItemType != null) { if (contract.Converter == null) contract.Converter = (JsonConverter)Activator.CreateInstance(typeof(ConcurrentBagConverter<>).MakeGenericType(new[] { concurrentItemType })); } return contract; } }Then, apply the generic version to your specific field as follows:
public class ComplexClass { [JsonConverter(typeof(ConcurrentBagConverter<SimpleClass>))] public ConcurrentBag<SimpleClass> _simpleClassObjects; }Or, apply a universal version globally for all ConcurrentBag<T> for any T using the following settings:
var settings = new JsonSerializerSettings { Converters = { new ConcurrentBagConverter() }, };Alternatively a custom contract resolver could be used, which might have slightly better performance than using the universal converter:
var settings = new JsonSerializerSettings { ContractResolver = new ConcurrentBagContractResolver(), };Example fiddle.
That being said, the above only work if the ConcurrentBag<T> property or field is read/write. If the member is read-only then I have found that Json.NET 9.0.1 will skip deserialization even if a converter is present because it infers that the collection member and contents are both read-only. (This may be a bug in JsonSerializerInternalReader.CalculatePropertyDetails().)
As a workaround, you could make the property be privately settable, and mark it with [JsonProperty]:
public class ComplexClass { ConcurrentBag<SimpleClass> m_simpleClassObjects = new ConcurrentBag<SimpleClass>(); [JsonConverter(typeof(ConcurrentBagConverter<SimpleClass>))] [JsonProperty] public ConcurrentBag<SimpleClass> _simpleClassObjects { get { return m_simpleClassObjects; } private set { m_simpleClassObjects = value; } } }Or use a surrogate array property, thereby eliminating the need for any sort of converter:
public class ComplexClass { readonly ConcurrentBag<SimpleClass> m_simpleClassObjects = new ConcurrentBag<SimpleClass>(); [JsonIgnore] public ConcurrentBag<SimpleClass> _simpleClassObjects { get { return m_simpleClassObjects; } } [JsonProperty("_simpleClassObjects")] SimpleClass[] _simpleClassObjectsArray { get { return _simpleClassObjects.ToArray(); } set { if (value == null) return; foreach (var item in value) _simpleClassObjects.Add(item); } } }更多推荐
发布评论