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.

Combination of ComparingByMember and IgnoreCyclicReferences doesn't work

See original GitHub issue

Description

If I keep it short: having override Equals on objects I have to disable using it with ComparingByMembers. But if I do so, the IgnoreCyclicReferences seem to stop working.

Here the details.

We have written a lot of NHibernate persistence tests, where we are using FluentAssertions BeEquivalentOf. After adding a recommended for NH Equals-override in the DomainBase class we’ve got multiple issues with FluentAssertions. We had to add a Configuration option ComparingByMembers<DomainBase>() to prevent FluentAssertions using Equals-override for comparing objects equivalence. And it worked nice for the most of cases, but has introduced another issue - now FluentAssertions seem to ignore the option IgnoreCyclicReferences()

Complete minimal example reproducing the issue

Here is the minimal example I could make to reproduce the issue:

[TestFixture]
public class FluentAssertionsIssueTest {
    public class Base {
        public Guid Id { get; set; }
        // NHibernate recommends overriding Equals for preventing issues with Sets and while working with multiple sessions, as well as issues with proxies in collections
        public override bool Equals(object obj)  {
            if (ReferenceEquals(this, obj))
                return true;
            return (obj is Base baseObj) && baseObj.Id == Id;
        }
    }

    public class Item : Base {
        public string Title { get; set; }
        public Item Previous { get; set; }
        public Item Next { get; set; }
    }

    [Test]
    public void IgnoreCyclicReferences_Broken()  {
        // imitating NHibernate save in one session
        var first = new Item() {Id = Guid.NewGuid(), Title = "First"};
        var second = new Item() {Id = Guid.NewGuid(), Title = "Second", Previous = first};
        first.Next = second;

        // imitating NHibernate load in another session
        var firstCopy = new Item() {Id = first.Id, Title = first.Title};
        var secondCopy = new Item() {Id = second.Id, Title = second.Title, Previous = firstCopy};
        firstCopy.Next = secondCopy;

        // I'd like to check, that all the properties could be persisted correctly.

        // "The maximum recursion depth was reached" is generated here (IgnoringCyclicReferences doesn't seem to work)
        firstCopy.Should().BeEquivalentTo(first, opt => opt.ComparingByMembers<Base>()
            .IgnoringCyclicReferences(), "they are equivalent");
        first.Title = "Changed";
        //  Removing Equals-override from Base class fixes FA issues, but introduces NHibernate-issues
        //  Removing ComparingByMembers-option removes "The maximum recursion depth was reached", but breaks this check (changed Title is not considered)
        firstCopy.Should().NotBeEquivalentTo(first, opt => opt.ComparingByMembers<Base>()
            .IgnoringCyclicReferences(), "after change");
    }
}

Expected behavior:

Deep object comparison should be done, handling cyclic references correctly. The test should pass

Actual behavior:

I cannot use deep object comparison on objects with overridden Equals and cyclic references. The test fails with message:

The maximum recursion depth was reached.  
The maximum recursion depth was reached.  
The maximum recursion depth was reached.  
The maximum recursion depth was reached.  

With configuration:
- Use declared types and members
- Compare enums by value
- Ignoring cyclic references
- Match member by name (or throw)
- Without automatic conversion.
- Be strict about the order of items in byte arrays

   bei FluentAssertions.Execution.LateBoundTestFramework.Throw(String message)
   bei FluentAssertions.Execution.CollectingAssertionStrategy.ThrowIfAny(IDictionary`2 context)
   bei FluentAssertions.Equivalency.EquivalencyValidator.AssertEquality(EquivalencyValidationContext context)

If I comment out override Equals everything works in FA, but introduces issues with NHibernate. If I remove options ComparingByMembers<Base>() in both BeEquivalentTo / NotBeEquivalentTo, then the difference of Title in firstCopy and first is not detected in the NotBeEquivalentTo-Check.

Versions

  • 5.10.3, 5.10.2
  • Using .NET framework 4.6.1

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:12 (7 by maintainers)

github_iconTop GitHub Comments

2reactions
dennisdoomencommented, Aug 27, 2020

It’s a bug. The EquivalencyValidator does not treat Item as a complex type because it overrides Equals, and the IsCyclicReference does not honor the overrides that ComparingByMembers sets.

1reaction
jnyrupcommented, Feb 21, 2021

This seems doable by changing EquivalencyValidator.IsComplexType(object expectation) from

isComplexType = !type.OverridesEquals();

to

isComplexType = config.GetEqualityStrategy(type) is EqualityStrategy.Members or EqualityStrategy.ForceMembers;

The change above does not kick in when formatting a failed assertion, as Formatter.ToString does not have access to config, so the output is

Expected firstCopy not to be equivalent to 

FluentAssertions.Specs.Equivalency.BasicEquivalencySpecs+Item1
{
   Id = {040c53d4-6c01-4f13-b2cf-8a757428a936}
   Next = FluentAssertions.Specs.Equivalency.BasicEquivalencySpecs+Item1
   {
      Id = {6c566e7d-1e94-4ad5-8a26-b5e5f08e8647}
      Next = <null>
      Previous = FluentAssertions.Specs.Equivalency.BasicEquivalencySpecs+Item1
      {
         Id = {040c53d4-6c01-4f13-b2cf-8a757428a936}
         Next = FluentAssertions.Specs.Equivalency.BasicEquivalencySpecs+Item1
         {
            Id = {6c566e7d-1e94-4ad5-8a26-b5e5f08e8647}
            Next = <null>
            Previous = FluentAssertions.Specs.Equivalency.BasicEquivalencySpecs+Item1
            {
               Id = {040c53d4-6c01-4f13-b2cf-8a757428a936}
               Next = FluentAssertions.Specs.Equivalency.BasicEquivalencySpecs+Item1
               {
                  Id = {Maximum recursion depth was reached…}
                  Next = {Maximum recursion depth was reached…}
                  Previous = {Maximum recursion depth was reached…}
                  Title = {Maximum recursion depth was reached…}
               }
               Previous = <null>
               Title = "First"
            }
            Title = "Second"
         }
         Previous = <null>
         Title = "First"
      }
      Title = "Second"
   }
   Previous = <null>
   Title = "First"
} because after change, but they are.

Including the check for Formatter.ToString seems more possible once #1469 is in.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Proposal: Add mechanism to handle circular references ...
Ignore: On Serialize: Ignores (skips writing) the property/element where the reference loop is detected. On Deserialize: No effect.
Read more >
How to fix nasty circular dependency issues once and for ...
Fix attempt 1. So, it turns out that our circular dependency causes a nasty problem. However, if we look closely it is pretty...
Read more >
Ignoring Circular References in JSON: Understanding the ...
Ignoring circular references can introduce data inconsistencies. Entities that should be connected may lose their relationships in the ...
Read more >
Is there a Java utility to do a deep comparison of two objects?
You can compare two objects of the same type and it will show changes, additions and removals. If there are no changes, then...
Read more >
code quality - What's wrong with circular references?
Circular references are a problem if you're using reference counts, but most garbage collectors are tracing style (where you start with the list ......
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