【问题标题】:How to perform partial object serialization providing "paths" using Newtonsoft JSON.NET如何使用 Newtonsoft JSON.NET 执行提供“路径”的部分对象序列化
【发布时间】:2015-05-18 13:09:44
【问题描述】:

我有一种情况,我有一个非常大的 C# 对象,但是,我只需要返回少数属性(可以在嵌套对象上),允许客户端 JavaScript 修改这些属性,然后发送将生成的对象返回给服务器,以便执行就地部分反序列化。

这个想法是重用一些非常大的现有业务对象,但要智能地仅序列化这些属性并将其发送回客户端应用程序进行修改(以将传输的数据量保持在最低限度)。

我基本上有一个 XML 文件,我在其中使用“路径语法”预定义所有绑定,这将仅指示我需要序列化的那些属性。所以,我可以使用“WorkOrder.UserField1”或“WorkOrder.Client.Name”之类的东西。

我尝试使用自定义合同解析器来确定是否应序列化属性;但是,我似乎没有关于“路径”的信息(换句话说,对象模型中的其他属性在链上)以确定该属性是否应该被序列化。

我也尝试过使用自定义 JsonTextWriter,但我似乎无法覆盖跟踪路径所需的方法,即使有可用的 Path 属性。为了能够查看正在序列化的属性的路径层次结构并确定是否应该通过在表中查找路径并做出决定来确定它是否应该被序列化,我是否可能忽略了一些简单的事情?

【问题讨论】:

  • 您是否希望以上下文无关或上下文相关的方式从类层次结构中修剪属性? IE。如果您有一个类Bar 和一个class Foo { public Bar Bar1 { get; set; } public Bar Bar2 { get; set; } },您是否有可能需要Bar1Bar2 的不同属性子集?
  • 您愿意序列化整个对象,然后过滤掉不需要的属性吗?这很简单,并且满足您减少传输数据量的要求。
  • @dbc 它将取决于上下文。基本上,我预先定义了一个表单“模板”,我将在其中生成 HTML 以及与对象属性的数据绑定。因此,我将从这个模板中收集所有唯一路径,但有可能两个路径可能具有相同的对象类型,但只使用不同的属性子集。
  • @dbc,如果有一种简单的方法可以在序列化后仅挑选与我感兴趣的“属性”路径匹配的值,我可以处理整个对象的序列化。

标签: json serialization json.net partial


【解决方案1】:

这里的基本难点在于 Json.NET 是一个基于契约的序列化器,它为要序列化的每个 type 创建一个契约,然后根据契约进行(反)序列化。如果一个类型出现在对象层次结构中的多个位置,则适用相同的协定。但是您希望根据给定类型在层次结构中的位置选择性地包含属性,这与基本的“一种类型的契约”设计相冲突。

解决此问题的一种快速方法是序列化为JObject,然后使用JToken.SelectTokens() 仅选择要返回的 JSON 数据,删除其他所有数据。由于SelectTokens 完全支持JSONPath query syntax,您可以选择性地包括使用数组和属性通配符或其他过滤器,例如:

"$.FirstLevel[*].Bar"

在根对象的名为 "FirstLevel" 的属性的所有数组成员中包含所有名为 "Bar" 的属性。

这应该会根据需要减少您的网络使用量,但不会在服务器上节省任何处理时间。

可以通过以下扩展方法完成删除:

public static partial class JsonExtensions
{
    public static TJToken RemoveAllExcept<TJToken>(this TJToken obj, IEnumerable<string> paths) where TJToken : JToken
    {
        if (obj == null || paths == null)
            throw new NullReferenceException();
        var keepers = new HashSet<JToken>(paths.SelectMany(path => obj.SelectTokens(path)), ObjectReferenceEqualityComparer<JToken>.Default);

        var keepersAndParents = new HashSet<JToken>(keepers.SelectMany(t => t.AncestorsAndSelf()), ObjectReferenceEqualityComparer<JToken>.Default);
        // Keep any token that is a keeper, or a child of a keeper, or a parent of a keeper
        // I.e. if you have a path ""$.A.B" and it turns out that B is an object, then everything
        // under B should be kept.
        foreach (var token in obj.DescendantsAndSelfReversed().Where(t => !keepersAndParents.Contains(t) && !t.AncestorsAndSelf().Any(p => keepers.Contains(p))))
            token.RemoveFromLowestPossibleParent();

        // Return the object itself for fluent style programming.
        return obj;
    }

    public static string SerializeAndSelectTokens<T>(T root, string[] paths, Formatting formatting = Formatting.None, JsonSerializerSettings settings = null)
    {
        var obj = JObject.FromObject(root, JsonSerializer.CreateDefault(settings));

        obj.RemoveAllExcept(paths);

        var json = obj.ToString(formatting);

        return json;
    }

    public static TJToken RemoveFromLowestPossibleParent<TJToken>(this TJToken node) where TJToken : JToken
    {
        if (node == null)
            return null;
        JToken toRemove;
        var property = node.Parent as JProperty;
        if (property != null)
        {
            // Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
            toRemove = property;
            property.Value = null;
        }
        else
        {
            toRemove = node;
        }
        if (toRemove.Parent != null)
            toRemove.Remove();
        return node;
    }

    public static IEnumerable<JToken> DescendantsAndSelfReversed(this JToken node)
    {
        if (node == null)
            throw new ArgumentNullException();
        return RecursiveEnumerableExtensions.Traverse(node, t => ListReversed(t as JContainer));
    }

    // Iterate backwards through a list without throwing an exception if the list is modified.
    static IEnumerable<T> ListReversed<T>(this IList<T> list)
    {
        if (list == null)
            yield break;
        for (int i = list.Count - 1; i >= 0; i--)
            yield return list[i];
    }
}

public static partial class RecursiveEnumerableExtensions
{
    // Rewritten from the answer by Eric Lippert https://stackoverflow.com/users/88656/eric-lippert
    // to "Efficient graph traversal with LINQ - eliminating recursion" http://stackoverflow.com/questions/10253161/efficient-graph-traversal-with-linq-eliminating-recursion
    // to ensure items are returned in the order they are encountered.

    public static IEnumerable<T> Traverse<T>(
        T root,
        Func<T, IEnumerable<T>> children)
    {
        yield return root;

        var stack = new Stack<IEnumerator<T>>();
        try
        {
            stack.Push((children(root) ?? Enumerable.Empty<T>()).GetEnumerator());

            while (stack.Count != 0)
            {
                var enumerator = stack.Peek();
                if (!enumerator.MoveNext())
                {
                    stack.Pop();
                    enumerator.Dispose();
                }
                else
                {
                    yield return enumerator.Current;
                    stack.Push((children(enumerator.Current) ?? Enumerable.Empty<T>()).GetEnumerator());
                }
            }
        }
        finally
        {
            foreach (var enumerator in stack)
                enumerator.Dispose();
        }
    }
}

/// <summary>
/// A generic object comparerer that would only use object's reference, 
/// ignoring any <see cref="IEquatable{T}"/> or <see cref="object.Equals(object)"/>  overrides.
/// </summary>
public class ObjectReferenceEqualityComparer<T> : IEqualityComparer<T> where T : class
{
    // Adapted from this answer https://stackoverflow.com/a/1890230
    // to https://stackoverflow.com/questions/1890058/iequalitycomparert-that-uses-referenceequals
    // By https://stackoverflow.com/users/177275/yurik
    private static readonly IEqualityComparer<T> _defaultComparer;

    static ObjectReferenceEqualityComparer() { _defaultComparer = new ObjectReferenceEqualityComparer<T>(); }

    public static IEqualityComparer<T> Default { get { return _defaultComparer; } }

    #region IEqualityComparer<T> Members

    public bool Equals(T x, T y)
    {
        return ReferenceEquals(x, y);
    }

    public int GetHashCode(T obj)
    {
        return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
    }

    #endregion
}

然后像这样使用它们:

public class TestClass
{
    public static void Test()
    {
        var root = new RootObject
        {
            FirstLevel1 = new FirstLevel
            {
                SecondLevel1 = new List<SecondLevel> { new SecondLevel { A = "a11", B = "b11", Third1 = new ThirdLevel { Foo = "Foos11", Bar = "Bars11" }, Third2 = new List<ThirdLevel> { new ThirdLevel { Foo = "FooList11", Bar = "BarList11" } } } },
                SecondLevel2 = new List<SecondLevel> { new SecondLevel { A = "a12", B = "b12", Third1 = new ThirdLevel { Foo = "Foos12", Bar = "Bars12" }, Third2 = new List<ThirdLevel> { new ThirdLevel { Foo = "FooList12", Bar = "BarList12" } } } },
            },
            FirstLevel2 = new FirstLevel
            {
                SecondLevel1 = new List<SecondLevel> { new SecondLevel { A = "a21", B = "b21", Third1 = new ThirdLevel { Foo = "Foos21", Bar = "Bars21" }, Third2 = new List<ThirdLevel> { new ThirdLevel { Foo = "FooList21", Bar = "BarList21" } } } },
                SecondLevel2 = new List<SecondLevel> { new SecondLevel { A = "a22", B = "b22", Third1 = new ThirdLevel { Foo = "Foos22", Bar = "Bars22" }, Third2 = new List<ThirdLevel> { new ThirdLevel { Foo = "FooList22", Bar = "BarList22" } } } },
            }
        };

        Assert.IsTrue(JObject.FromObject(root).DescendantsAndSelf().OfType<JValue>().Count() == 24); // No assert

        var paths1 = new string[] 
        {
            "$.FirstLevel2.SecondLevel1[*].A",
            "$.FirstLevel1.SecondLevel2[*].Third2[*].Bar",
        };

        Test(root, paths1, 2);

        var paths3 = new string[] 
        {
            "$.FirstLevel1.SecondLevel2[*].Third2[*].Bar",
        };

        Test(root, paths3, 1);

        var paths4 = new string[] 
        {
            "$.*.SecondLevel2[*].Third2[*].Bar",
        };

        Test(root, paths4, 2);
    }

    static void Test<T>(T root, string [] paths, int expectedCount)
    {
        var json = JsonExtensions.SerializeAndSelectTokens(root, paths, Formatting.Indented);
        Console.WriteLine("Result using paths: {0}", JsonConvert.SerializeObject(paths));
        Console.WriteLine(json);
        Assert.IsTrue(JObject.Parse(json).DescendantsAndSelf().OfType<JValue>().Count() == expectedCount); // No assert
    }
}

public class ThirdLevel
{
    public string Foo { get; set; }
    public string Bar { get; set; }
}

public class SecondLevel
{
    public ThirdLevel Third1 { get; set; }
    public List<ThirdLevel> Third2 { get; set; }

    public string A { get; set; }
    public string B { get; set; }
}

public class FirstLevel
{
    public List<SecondLevel> SecondLevel1 { get; set; }
    public List<SecondLevel> SecondLevel2 { get; set; }
}

public class RootObject
{
    public FirstLevel FirstLevel1 { get; set; }
    public FirstLevel FirstLevel2 { get; set; }
}

请注意,有一个增强请求 Feature request: ADD JsonProperty.ShouldSerialize(object target, string path) #1857 可以更轻松地启用此类功能。

演示小提琴herehere

【讨论】:

  • 这看起来可以完成我想要做的事情。我什至没有想到 JObject 方法,也不知道有 JSON Path 通配符语法! (感谢您的启发。)就我而言,不需要使用通配符,所有路径都是明确的。 (即“p1.p2.p3”,其中每个属性由句点分隔是父对象层次结构,“p3”最终是一个简单类型(如字符串,int等)而不是一个对象。让我把我的头围绕这个,如果它满足我的需要,我会欠你一个“答案”确认和一杯啤酒!
  • 做得很好,谢谢。现在我必须为它编写测试:)
  • 您好,感谢您提供出色的代码。我面临的唯一问题是是否有重复的属性,比如说,所有 A 都是“a11”,如果我给出 A 的路径,它只会给出第一次出现。守护者获得唯一值,而守护者和父母只有第一条路径,因为它不会检查相同值再次出现的位置。如果你能告诉我如何解决这个问题,那将有很大帮助
【解决方案2】:

更简单的实现(与accepted answer 相比)呈现在here

public static class JsonExtensions
{
    public static TJToken RemoveAllExcept<TJToken>(this TJToken token, IEnumerable<string> paths) where TJToken : JContainer
    {
        HashSet<JToken> nodesToRemove = new(ReferenceEqualityComparer.Instance);
        HashSet<JToken> nodesToKeep = new(ReferenceEqualityComparer.Instance);

        foreach (var whitelistedToken in paths.SelectMany(token.SelectTokens))
            TraverseTokenPath(whitelistedToken, nodesToRemove, nodesToKeep);

        //In that case neither path from paths has returned any token
        if (nodesToKeep.Count == 0)
        {
            token.RemoveAll();
            return token;
        }

        nodesToRemove.ExceptWith(nodesToKeep);

        foreach (var notWhitelistedNode in nodesToRemove)
            notWhitelistedNode.Remove();

        return token;
    }

    private static void TraverseTokenPath(JToken value, ISet<JToken> nodesToRemove, ISet<JToken> nodesToKeep)
    {
        JToken? immediateValue = value;

        do
        {
            nodesToKeep.Add(immediateValue);

            if (immediateValue.Parent is JObject or JArray)
            {
                foreach (var child in immediateValue.Parent.Children())
                    if (!ReferenceEqualityComparer.Instance.Equals(child, value))
                        nodesToRemove.Add(child);
            }

            immediateValue = immediateValue.Parent;
        } while (immediateValue != null);
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-11-10
    • 1970-01-01
    • 2017-05-26
    • 2018-11-09
    • 1970-01-01
    • 2016-09-10
    • 2011-07-21
    相关资源
    最近更新 更多