如何使用 Newtonsoft JSON.NET 提供 "paths" 执行部分对象序列化

How to perform partial object serialization providing "paths" using Newtonsoft JSON.NET

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

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

我基本上有一个 XML 文件,我在其中使用 "path syntax" 预定义了所有绑定,这将仅指示我需要序列化的那些属性。所以,我可以使用 "WorkOrder.UserField1" 或 "WorkOrder.Client.Name".

之类的东西

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

我也尝试过使用自定义 JsonTextWriter,但我似乎无法覆盖跟踪路径所需的方法,即使有可用的路径 属性。为了能够查看正在序列化的 属性 的路径层次结构并确定是否应该通过查找 table 中的路径并使决定?

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

解决此问题的一种快速方法是序列化为 JObject, then use JToken.SelectTokens() to select only the JSON data you want to return, removing everything else. Since SelectTokens has full support for 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://whosebug.com/users/88656/eric-lippert
    // to "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 
    // to 
    // By https://whosebug.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 可以更轻松地启用此类功能。

小提琴演示 here and here

更容易实施(与 ) is presented :

相比
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);
    }
}