一尘不染

如何使用Newtonsoft JSON.NET执行提供“路径”的部分对象序列化

json

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

这个想法是重新使用一些非常大的现有业务对象,但是只对序列化并将那些属性仅发送回客户端应用程序进行修改(以使传输的数据量保持最小)是明智的。

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

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

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


阅读 258

收藏
2020-07-27

共1个答案

一尘不染

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

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

"$.FirstLevel[*].Bar"

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

这样可以减少所需的网络使用量,但不会节省服务器上的任何处理时间。

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

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; }
}

请注意,有一个增强功能请求:ADD
JsonProperty.ShouldSerialize(对象目标,字符串路径)#1857
,可以更轻松地启用这种功能。

演示在这里这里摆弄。

2020-07-27