【问题标题】:Populate object where objects are reused and arrays are replaced?在对象被重用和数组被替换的地方填充对象?
【发布时间】:2017-02-11 04:22:27
【问题描述】:

使用 Json.NET,我想将 JObject 映射到具有以下行为的 .NET 对象:

  1. 对于JObject 的(非空)数组属性,替换目标对象上的整个集合。

  2. 对于JObject 的(非空)对象属性,如果它不为空,则重用目标对象的属性,并将提供的属性映射到其上。

JsonSerializer.Populate 似乎是我想要的,如this answer 中所述。至于我正在寻找的行为,我似乎可以通过JsonSerializerSettings.ObjectCreationHandling 实现其中之一,但不能同时实现两者。 ObjectCreationHandling.Replace 根据要求 #1 执行我想要的操作,而 ObjectCreationHandling.Auto 对于要求 #2 执行我想要执行的操作,但是它将数组项附加到现有集合中。

这里推荐的实现这两个要求的方法是什么?

【问题讨论】:

  • 应该注意的是,JObject.Merge 提供了一种通过MergeArrayHandling 配置数组行为的好方法。但这是针对 JObject 到 JObject 的映射,而不是 JObject 到 .NET 对象。很遗憾,此设置在 JsonSerializerSettings 中不可用。

标签: c# json.net


【解决方案1】:

Json.NET 将自动替换任何数组或只读集合。要在反序列化时清除读写集合,您可以创建一个custom contract resolver,将OnDeserializingCallback 添加到反序列化开始时清除集合的每个可修改集合。清除集合而不是直接替换它可以处理集合仅获取的情况,例如:

public class RootObject
{
    readonly HashSet<int> hashSet = new HashSet<int>();
    public HashSet<int> HashSetValues { get { return this.hashSet; } }
}

合约解析器如下:

public class CollectionClearingContractResolver : DefaultContractResolver
{
    static void ClearGenericCollectionCallback<T>(object o, StreamingContext c)
    {
        var collection = o as ICollection<T>;
        if (collection == null || collection is Array || collection.IsReadOnly)
            return;
        collection.Clear();
    }

    static SerializationCallback ClearListCallback = (o, c) =>
        {
            var collection = o as IList;
            if (collection == null || collection is Array || collection.IsReadOnly)
                return;
            collection.Clear();
        };

    protected override JsonArrayContract CreateArrayContract(Type objectType)
    {
        var contract = base.CreateArrayContract(objectType);
        if (!objectType.IsArray)
        {
            if (typeof(IList).IsAssignableFrom(objectType))
            {
                contract.OnDeserializingCallbacks.Add(ClearListCallback);
            }
            else if (objectType.GetCollectItemTypes().Count() == 1)
            {
                MethodInfo method = typeof(CollectionClearingContractResolver).GetMethod("ClearGenericCollectionCallback", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
                MethodInfo generic = method.MakeGenericMethod(contract.CollectionItemType);
                contract.OnDeserializingCallbacks.Add((SerializationCallback)Delegate.CreateDelegate(typeof(SerializationCallback), generic));
            }
        }

        return contract;
    }
}

public static class TypeExtensions
{
    public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
    {
        if (type == null)
            throw new ArgumentNullException();
        if (type.IsInterface)
            return new[] { type }.Concat(type.GetInterfaces());
        else
            return type.GetInterfaces();
    }

    public static IEnumerable<Type> GetCollectItemTypes(this Type type)
    {
        foreach (Type intType in type.GetInterfacesAndSelf())
        {
            if (intType.IsGenericType
                && intType.GetGenericTypeDefinition() == typeof(ICollection<>))
            {
                yield return intType.GetGenericArguments()[0];
            }
        }
    }
}

public static class JsonExtensions
{
    public static void Populate<T>(this JToken value, T target) where T : class
    {
        value.Populate(target, null);
    }

    public static void Populate<T>(this JToken value, T target, JsonSerializerSettings settings) where T : class
    {
        using (var sr = value.CreateReader())
        {
            JsonSerializer.CreateDefault(settings).Populate(sr, target);
        }
    }
}

然后要使用它,请执行以下操作:

var settings = new JsonSerializerSettings
{
    ContractResolver = new CollectionClearingContractResolver(),
};
jObject.Populate(rootObject, settings);

示例fiddle。

这样的契约解析器在反序列化在默认构造函数中填充集合的对象时也很有用,例如Deserialization causes copies of List-Entries。

【讨论】:

  • 感谢您的详尽回答。可能比我所做的工作效率更高,尽管反射等会产生更复杂的解决方案。基石权衡我的选择,但再次感谢。
【解决方案2】:

一种解决方法是使用自定义JsonConverter,它通过在检测到集合类型时忽略现有值来有效替换集合。

public class ReplaceArrayConverter : JsonConverter
{
    public override bool CanConvert(Type objectType) {
        // check for Array, IList, etc.
        return objectType.IsCollection(); 
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
        // ignore existingValue and just create a new collection
        return JsonSerializer.CreateDefault().Deserialize(reader, objectType);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
        JsonSerializer.CreateDefault().Serialize(writer, value);
    }
}

这样使用:

var ser = JsonSerializer.CreateDefault(new JsonSerializerSettings {
    Converters = new[] { new ReplaceArrayConverter() }
});

using (var reader = jObj.CreateReader()) {
    ser.Populate(reader, model);
}

【讨论】:

  • 其实你需要做的就是忽略existingValue并返回一个新的集合。
  • @dbc 我注意到在ReadJson 中调用serializer.Deserialize(reader, objectType) 会让我陷入无限循环。很容易看出为什么。您认为使用JsonSerializer.CreateDefault() 是正确的方法吗?它有效,但不确定是否有更简单/更有效的方法。
  • 这就是我建议使用合约解析器的原因之一。您还可以按照WriteJson() 的行从this answer 禁用递归调用转换器。这样做可以保留您的其他序列化程序设置。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-15
  • 1970-01-01
  • 2013-03-10
  • 2013-06-30
  • 2012-01-24
  • 1970-01-01
相关资源
最近更新 更多