Endless recursion when same serializer instance is reused (referential integrity is enabled)
See original GitHub issueHey 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.
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:
- Created 3 years ago
- Comments:9 (6 by maintainers)
Top GitHub Comments
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!
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:
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. 👍