Могу ли я заставить Json. NET начать десериализацию дерева с root - PullRequest
1 голос
/ 13 июля 2020

Это моя первая попытка сериализовать / десериализовать дерево. Работает отлично, за исключением Path. Он должен быть создан из root, но десериализация начинается с листьев.

    var root = new Node(null, "rootName");
    var tree = new Tree(root);
    root.AddChild("childName");
    var str = JsonConvert.SerializeObject(tree, Newtonsoft.Json.Formatting.Indented);
    var treeRestored = JsonConvert.DeserializeObject<Tree>(str);

...

class Node
{
    public IReadOnlyList<Node> Children => _children;

    [JsonIgnore]
    public string Path { get; } // needs parent

    [JsonProperty(ReferenceLoopHandling = ReferenceLoopHandling.Ignore)]
    public Node Parent { get; private set; }

    public string Name { get; }

    public Node(Node parent, string name)
    {
        Name = name;
        Parent = parent;
        Path = (parent == null ? "" : (parent.Name + ".")) + name;
        _children = new List<Node>();
    }

    [JsonConstructor]
    private Node(string Name, List<Node> Children)
    {
        this.Name = Name;
        _children = Children;
        foreach (var child in _children)
        {
            child.Parent = this;
        }
    }

    public void AddChild(string name)
    {
        _children.Add(new Node(this, name));
    }

    private readonly List<Node> _children;
}

class Tree
{
    public Node Root;

    public Tree(Node root)
    {
        Root = root;
    }
}

Итак, я попробовал следующее. Я удалил JsonProperty из свойства Parent, изменил команду сериализации и изменил JSON конструктор.

    var str = JsonConvert.SerializeObject(tree, Newtonsoft.Json.Formatting.Indented,
        new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects });

    [JsonConstructor]
    private Node(string name, Node parent)
    {
        Name = name;
        Parent = parent;
        if (Parent != null) // Rebuild path
        {
            parent._children.Add(this);
            Path = parent.Name + "." + Name;
        }
        else
        {
            Path = Name;
        }
        _children = new List<Node>();
    }

С этими изменениями строка сериализации выглядит так:

{
  "$id": "1",
  "Root": {
    "$id": "2",
    "Children": [
      {
        "$id": "3",
        "Children": [],
        "Parent": {
          "$ref": "2"
        },
        "Name": "childName"
      }
    ],
    "Parent": null,
    "Name": "rootName"
  }
}

Эта строка Json содержит достаточно информации, чтобы сначала создать root, а затем дочерний элемент. Но JsonConverter все равно начинается с листьев. Как я могу изменить порядок?

Я знаю, что могу использовать OnDeserialized, но это последнее средство для меня.

1 Ответ

1 голос
/ 13 июля 2020

Я думаю, что часть проблемы заключается в том, что вы пытаетесь использовать параметризованный конструктор с PerserveReferencesHandling.Objects, который задокументирован , чтобы не работать:

Примечание : Ссылки не могут быть сохранены, если значение установлено через конструктор, отличный от конструктора по умолчанию. В конструкторе, отличном от конструктора по умолчанию, дочерние значения должны быть созданы перед родительским значением, чтобы их можно было передать в конструктор, что делает невозможным отслеживание ссылки. Типы ISerializable являются примером класса, значения которого заполняются конструктором не по умолчанию и не будут работать с PreserveReferencesHandling.

Вот что я бы сделал вместо этого:

  1. Удалите параметры из закрытого [JsonConstructor] и заставьте этот конструктор просто инициализировать дочерние элементы пустым списком.

    [JsonConstructor]
    private Node()
    {
        _children = new List<Node>();
    }
    
  2. Добавьте частные сеттеры к Name и Parent и пометьте их [JsonProperty], чтобы Json. Net мог использовать частные сеттеры:

    [JsonProperty]  // allow Json.Net to use private setter
    public Node Parent { get; private set; }
    
    [JsonProperty]  // allow Json.Net to use private setter
    public string Name { get; private set; }
    
  3. Не храните Path в своем объекте; вместо этого рассчитайте его по запросу. Это всего лишь одна строка кода, и она быстрая, потому что она проходит вверх по цепочке.

    [JsonIgnore]
    public string Path
    {
        get { return Parent != null ? Parent.Path + "." + Name : Name; }
    }
    
  4. (необязательно, но рекомендуется) Сделайте свой AddChild метод возвращает Node, который он создает - это значительно упростит его использование при создании дерева. В противном случае вам всегда придется искать в списке дочерних элементов только что добавленный узел.

    public Node AddChild(string name)
    {
        Node node = new Node(this, name);
        _children.Add(node);
        return node;
    }
    

Вот как будет выглядеть ваш класс после всех изменений:

class Node
{
    public IReadOnlyList<Node> Children => _children;

    [JsonIgnore]
    public string Path
    {
        get { return Parent != null ? Parent.Path + "." + Name : Name; }
    }

    [JsonProperty]  // allow Json.Net to use private setter
    public Node Parent { get; private set; }

    [JsonProperty]  // allow Json.Net to use private setter
    public string Name { get; private set; }

    public Node(Node parent, string name)
    {
        Name = name;
        Parent = parent;
        _children = new List<Node>();
    }

    [JsonConstructor]
    private Node()
    {
        _children = new List<Node>();
    }

    public Node AddChild(string name)
    {
        Node node = new Node(this, name);
        _children.Add(node);
        return node;
    }

    private readonly List<Node> _children;
}

Если вычисление Path "на лету" слишком дорого, вы можете попробовать следующие идеи:

  1. Используйте ленивое вычисление. Вычислите путь при первом обращении к нему, затем кешируйте результат локально для более быстрого последующего извлечения:

     private string _path;
    
     [JsonIgnore]
     public string Path
     {
         get 
         { 
             if (_path == null)
             {
                 _path = Parent != null ? Parent.Path + "." + Name : Name;
             }
             return _path; 
         }
     }
    
  2. Сразу после десериализации пройдите по дереву и запросите Path для каждого node, заставляя его рассчитываться и кешироваться. Затем он будет кеширован для каждого узла.

     void PrecachePaths(Node node)
     {
         var path = node.Path;
         foreach (Node child in node.Children)
         {
             PrecachePaths(child);
         }
     }
    
     var treeRestored = JsonConvert.DeserializeObject<Tree>(str);
     PrecachePaths(treeRestored.Root);
    
...