question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Endless recursion when same serializer instance is reused (referential integrity is enabled)

See original GitHub issue

Hey guys,

I’ve used ExtendedXmlSerializer successfully in one of our projects, but came across an endless recursion yesterday. Just to be clear, this bug can be easily circumvented by not reusing the same serializer instance on different calls to serialize. So the priority of this issue should be low.

I can provide a minimal, complete, verifiable example (MCVE) that reproduces the issue which you can find below. Just some general information: we have a complex object graph consisting of a map that has several nodes on them which are connected to each other via node links. When I first serialize the map, everything works fine. When I then change the map and serialize it again using the same serializer instance, it runs into an endless recursion, serializing the same node and node link again and again until a StackOverflowException occurs. As I said, this can be circumvented by using a new instance of the serializer.

Excerpt of XML document with endless recursion

The MCVE uses .NET Core 3.1 with xunit 2.4.1 and ExtendedXmlSerializer 3.2.5. Our project actually runs on .NET 4.7.2, so the issue should be independent of the target framework.

using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using ExtendedXmlSerializer;
using ExtendedXmlSerializer.Configuration;
using Xunit;

namespace ExtendedXmlSerializerEndlessRecursion
{
    public static class EndlessRecursionError
    {
        private static readonly XmlWriterSettings WriteIndentedSettings = new XmlWriterSettings { Indent = true, IndentChars = "    " };

        [Fact]
        public static void EndlessRecursionOnSecondCallToSerialize()
        {
            const string fileName = "endless-recursion.xml";
            var serializer = CreateDefaultSerializer();

            var map = new Map();
            var node1 = map.AddEntity(new Node(Guid.NewGuid()));
            var node2 = map.AddEntity(new Node(Guid.NewGuid()));
            map.AddEntity(NodeLink.CreateAttachedNodeLink(node1, node2));

            serializer.SerializeIndented(fileName, map);

            var node3 = map.AddEntity(new Node(Guid.NewGuid()));
            var node4 = map.AddEntity(new Node(Guid.NewGuid()));
            map.AddEntity(NodeLink.CreateAttachedNodeLink(node3, node4));

            //serializer = CreateDefaultSerializer(); // if this is uncommented, then the test succeeds. It seems the serializer keeps some state after the last serialization run that results in the stack overflow.
            serializer.SerializeIndented(fileName, map);
        }

        private static IExtendedXmlSerializer CreateDefaultSerializer() =>
            new ConfigurationContainer().UseOptimizedNamespaces()
                                        .EnableParameterizedContentWithPropertyAssignments()
                                        .UseAutoFormatting()
                                        .Type<Node>()
                                        .EnableReferences(node => node.Id)
                                        .Type<NodeLink>()
                                        .EnableReferences(nodeLink => nodeLink.Id)
                                        .Create();

        private static void SerializeIndented(this IExtendedXmlSerializer serializer, string fileName, object instance)
        {
            using var stream = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
            serializer.Serialize(WriteIndentedSettings, stream, instance);
        }
    }

    public abstract class Entity : IEquatable<Entity>
    {
        protected Entity(Guid id)
        {
            Id = id;
        }

        public Guid Id { get; }

        public bool Equals(Entity? other)
        {
            if (ReferenceEquals(this, other))
                return true;
            if (other is null)
                return false;

            return Id == other.Id;
        }

        public override bool Equals(object? obj) =>
            obj is Entity entity && Equals(entity);

        public override int GetHashCode() => Id.GetHashCode();
    }

    public sealed class Map
    {
        private List<Node>? _nodes;
        private List<NodeLink>? _nodeLinks;

        public List<Node> Nodes
        {
            get => _nodes ??= new List<Node>();
            set => _nodes = value;
        }

        public List<NodeLink> NodeLinks
        {
            get => _nodeLinks ??= new List<NodeLink>();
            set => _nodeLinks = value;
        }

        public T AddEntity<T>(T entity)
            where T : Entity
        {
            switch (entity)
            {
                case Node node:
                    Nodes.Add(node);
                    break;
                case NodeLink nodeLink:
                    NodeLinks.Add(nodeLink);
                    break;
            }

            return entity;
        }
    }

    public sealed class Node : Entity
    {
        private List<NodeLink>? _nodeLinks;

        public Node(Guid id) : base(id) { }

        public List<NodeLink> NodeLinks
        {
            get => _nodeLinks ??= new List<NodeLink>();
            set => _nodeLinks = value;
        }

        public void AddNodeLink(NodeLink nodeLink)
        {
            if (!ReferenceEquals(this, nodeLink.Node1) && !ReferenceEquals(this, nodeLink.Node2))
                throw new ArgumentException($"Node {Id} is not referenced from the node link {nodeLink.Id}, yet you want to attach it to this node.", nameof(nodeLink));

            NodeLinks.Add(nodeLink);
        }
    }

    public sealed class NodeLink : Entity
    {
        public NodeLink(Guid id) : base(id) { }

        public Node? Node1 { get; set; }

        public Node? Node2 { get; set; }

        public static NodeLink CreateAttachedNodeLink(Node node1, Node node2)
        {
            var nodeLink = new NodeLink(Guid.NewGuid()) { Node1 = node1, Node2 = node2 };
            node1.AddNodeLink(nodeLink);
            node2.AddNodeLink(nodeLink);
            return nodeLink;
        }
    }
}

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:9 (6 by maintainers)

github_iconTop GitHub Comments

1reaction
feO2xcommented, Aug 28, 2020

Thanks for your quick reply. Just take as much time as you need, this isn’t a show-stopper. And again, thank you for all your efforts!

1reaction
Mike-E-angelocommented, Aug 28, 2020

Yikes @feO2x it looks like we have some sort of errant caching going on with the references. I was able to reproduce it by reducing it to the following:

        [Fact]
        public static void EndlessRecursionOnSecondCallToSerialize()
        {
            var serializer = CreateDefaultSerializer();

            var map = new Map();

            serializer.Serialize(map).Should().NotBeEmpty();

            var node3 = map.AddEntity(new Node(Guid.NewGuid()));
            var node4 = map.AddEntity(new Node(Guid.NewGuid()));
            map.AddEntity(NodeLink.CreateAttachedNodeLink(node3, node4));

            serializer.Serialize(map).Should().NotBeEmpty();
        }

It would seem that after the first serialization, the object graph is stored somehow, and it is not being recompiled on the 2nd serialization, thus leading to the infinite recursion of doom. Creating a new serializer starts with a new cache, so that is why you are able to work around this (luckily, but of course not efficiently).

Thank you for reporting this. I will see if I can find some time today/this weekend to further check it out and hopefully get this fixed for you. 👍

Read more comments on GitHub >

github_iconTop Results From Across the Web

Django rest framework nested self-referential objects
Instead of using ManyRelatedField, use a nested serializer as your field: class SubCategorySerializer(serializers.
Read more >
DataSerializable (Apache Geode 1.15.1)
Attempting to deserialize an object graph that contains multiple reference paths to the same object will result in multiple copies of the objects...
Read more >
Allowing deserialization of LDAP objects is security-sensitive
JNDI supports the deserialization of objects from LDAP directories, which can lead to remote code execution.
Read more >
Everything You Need to Know About Java Serialization ...
The serialVersionUID is used to verify that the serialized and deserialized objects have the same attributes and thus are compatible with ...
Read more >
hazelcast-full-example.yaml
Open-source distributed computation and storage platform. Hazelcast is a real-time stream processing platform that lets you build applications that take ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found